Skip to content
Snippets Groups Projects
Unverified Commit c629e7dc authored by Adam Michaleski's avatar Adam Michaleski Committed by GitHub
Browse files

Add integration for Schluter (#31088)


* Add integration for Schluter
New integration for Schluter DITRA-HEAT thermostats. Interacts with Schluter API via py-schluter to implement climate platform.

* Fix for manifest issue on build
Removed unnecessary configurator logic

* Flake8 & ISort issues fixed

* Code review modifications for Schluter integration
- Removed unnecessary strings.json file
- Removed unnecessary DEFAULT_SCAN_INTERVAL from __init__.py
- Removed persistent notification for authentication error in __init__.py
- Removed string casts from log statements in __init__.py
- Removed unnecessary logging of entity creations in climate.py
- Removed unnecessary lookup for hvac_mode in climate.py

* Work started on Update Coordinator

* Further Coordinator work

* Update to DataUpdateCoordinator complete

* Flake8 fix

* Removal of unnecessary SchluterPlatformData
Coordinator also now using SCAN_INTERVAL

* Disable polling, use coordinator listeners

* Isort on climate.py

* Code review modifications
- Hass data stored under domain
- Since platform only likely to be climate, removed array, load explicitly
- Remove unnecessary fan mode implementation

* Switch to HVAC action for showing current heating status

* Isort fix

* HVAC Mode return list, set_hvac_mode function added

* __init__.py modifications from code review

* Climate.py code review modifications
- Device info property removed
- Write state function on set temp failure
- Add "available" property
- Delegate sync function in coordinator update to the executor

* Serial number as unique identifier
- Now using serial number & thermostat dictionary to protect against scenarios where a device is removed and enumerable index is no longer accurate
- Removed async_write_ha_state() from set temp exception, not needed
- Removed unnecessary SCAN_INTERVAL definition

* Linting

* Update homeassistant/components/schluter/climate.py

Co-Authored-By: default avatarPaulus Schoutsen <paulus@home-assistant.io>

* Update homeassistant/components/schluter/climate.py

Co-Authored-By: default avatarPaulus Schoutsen <paulus@home-assistant.io>

* Changed timeout from config, load platform discover to None

* Proposed change for discover info issue

Co-authored-by: default avatarPaulus Schoutsen <paulus@home-assistant.io>
parent 73c52e66
No related branches found
No related tags found
No related merge requests found
......@@ -609,6 +609,7 @@ omit =
homeassistant/components/saj/sensor.py
homeassistant/components/salt/device_tracker.py
homeassistant/components/satel_integra/*
homeassistant/components/schluter/*
homeassistant/components/scrape/sensor.py
homeassistant/components/scsgate/*
homeassistant/components/scsgate/cover.py
......
......@@ -313,6 +313,7 @@ homeassistant/components/saj/* @fredericvl
homeassistant/components/salt/* @bjornorri
homeassistant/components/samsungtv/* @escoand
homeassistant/components/scene/* @home-assistant/core
homeassistant/components/schluter/* @prairieapps
homeassistant/components/scrape/* @fabaff
homeassistant/components/script/* @home-assistant/core
homeassistant/components/search/* @home-assistant/core
......
"""The Schluter DITRA-HEAT integration."""
import logging
from requests import RequestException, Session
from schluter.api import Api
from schluter.authenticator import AuthenticationState, Authenticator
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHLUTER_SESSION = "schluter_session"
DATA_SCHLUTER_API = "schluter_api"
SCHLUTER_CONFIG_FILE = ".schluter.conf"
API_TIMEOUT = 10
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(DOMAIN): vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
def setup(hass, config):
"""Set up the Schluter component."""
_LOGGER.debug("Starting setup of schluter")
conf = config[DOMAIN]
api_http_session = Session()
api = Api(timeout=API_TIMEOUT, http_session=api_http_session)
authenticator = Authenticator(
api,
conf.get(CONF_USERNAME),
conf.get(CONF_PASSWORD),
session_id_cache_file=hass.config.path(SCHLUTER_CONFIG_FILE),
)
authentication = None
try:
authentication = authenticator.authenticate()
except RequestException as ex:
_LOGGER.error("Unable to connect to Schluter service: %s", ex)
return
state = authentication.state
if state == AuthenticationState.AUTHENTICATED:
hass.data[DOMAIN] = {
DATA_SCHLUTER_API: api,
DATA_SCHLUTER_SESSION: authentication.session_id,
}
discovery.load_platform(hass, "climate", DOMAIN, {}, config)
return True
if state == AuthenticationState.BAD_PASSWORD:
_LOGGER.error("Invalid password provided")
return False
if state == AuthenticationState.BAD_EMAIL:
_LOGGER.error("Invalid email provided")
return False
_LOGGER.error("Unknown set up error: %s", state)
return False
"""Support for Schluter thermostats."""
import logging
from requests import RequestException
import voluptuous as vol
from homeassistant.components.climate import (
PLATFORM_SCHEMA,
SCAN_INTERVAL,
ClimateDevice,
)
from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
HVAC_MODE_HEAT,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import ATTR_TEMPERATURE, CONF_SCAN_INTERVAL
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from . import DATA_SCHLUTER_API, DATA_SCHLUTER_SESSION, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1))}
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Schluter thermostats."""
if discovery_info is None:
return
session_id = hass.data[DOMAIN][DATA_SCHLUTER_SESSION]
api = hass.data[DOMAIN][DATA_SCHLUTER_API]
temp_unit = hass.config.units.temperature_unit
async def async_update_data():
try:
thermostats = await hass.async_add_executor_job(
api.get_thermostats, session_id
)
except RequestException as err:
raise UpdateFailed(f"Error communicating with Schluter API: {err}")
if thermostats is None:
return {}
return {thermo.serial_number: thermo for thermo in thermostats}
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="schluter",
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
)
await coordinator.async_refresh()
async_add_entities(
SchluterThermostat(coordinator, serial_number, temp_unit, api, session_id)
for serial_number, thermostat in coordinator.data.items()
)
class SchluterThermostat(ClimateDevice):
"""Representation of a Schluter thermostat."""
def __init__(self, coordinator, serial_number, temp_unit, api, session_id):
"""Initialize the thermostat."""
self._unit = temp_unit
self._coordinator = coordinator
self._serial_number = serial_number
self._api = api
self._session_id = session_id
self._support_flags = SUPPORT_TARGET_TEMPERATURE
@property
def available(self):
"""Return if thermostat is available."""
return self._coordinator.last_update_success
@property
def should_poll(self):
"""Return if platform should poll."""
return False
@property
def supported_features(self):
"""Return the list of supported features."""
return self._support_flags
@property
def unique_id(self):
"""Return unique ID for this device."""
return self._serial_number
@property
def name(self):
"""Return the name of the thermostat."""
return self._coordinator.data[self._serial_number].name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit
@property
def current_temperature(self):
"""Return the current temperature."""
return self._coordinator.data[self._serial_number].temperature
@property
def hvac_mode(self):
"""Return current mode. Only heat available for floor thermostat."""
return HVAC_MODE_HEAT
@property
def hvac_action(self):
"""Return current operation. Can only be heating or idle."""
return (
CURRENT_HVAC_HEAT
if self._coordinator.data[self._serial_number].is_heating
else CURRENT_HVAC_IDLE
)
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._coordinator.data[self._serial_number].set_point_temp
@property
def hvac_modes(self):
"""List of available operation modes."""
return [HVAC_MODE_HEAT]
@property
def min_temp(self):
"""Identify min_temp in Schluter API."""
return self._coordinator.data[self._serial_number].min_temp
@property
def max_temp(self):
"""Identify max_temp in Schluter API."""
return self._coordinator.data[self._serial_number].max_temp
async def async_set_hvac_mode(self, hvac_mode):
"""Mode is always heating, so do nothing."""
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temp = None
target_temp = kwargs.get(ATTR_TEMPERATURE)
serial_number = self._coordinator.data[self._serial_number].serial_number
_LOGGER.debug("Setting thermostat temperature: %s", target_temp)
try:
if target_temp is not None:
self._api.set_temperature(self._session_id, serial_number, target_temp)
except RequestException as ex:
_LOGGER.error("An error occurred while setting temperature: %s", ex)
async def async_added_to_hass(self):
"""When entity is added to hass."""
self._coordinator.async_add_listener(self.async_write_ha_state)
async def async_will_remove_from_hass(self):
"""When entity will be removed from hass."""
self._coordinator.async_remove_listener(self.async_write_ha_state)
"""Constants for the Schluter DITRA-HEAT integration."""
DOMAIN = "schluter"
{
"domain": "schluter",
"name": "Schluter",
"documentation": "https://www.home-assistant.io/integrations/schluter",
"requirements": ["py-schluter==0.1.7"],
"dependencies": [],
"codeowners": ["@prairieapps"]
}
......@@ -1111,6 +1111,9 @@ py-cpuinfo==5.0.0
# homeassistant.components.melissa
py-melissa-climate==2.0.0
# homeassistant.components.schluter
py-schluter==0.1.7
# homeassistant.components.synology
py-synology==0.2.0
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment