From 73c045d4cc20af47411290c42b45be34445de873 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 May 2022 00:08:40 +0200 Subject: [PATCH 01/18] Various changes --- custom_components/danfoss_ally/__init__.py | 20 +- .../danfoss_ally/binary_sensor.py | 9 +- custom_components/danfoss_ally/climate.py | 60 ++++-- custom_components/danfoss_ally/manifest.json | 28 +-- .../danfoss_ally/pydanfossally/__init__.py | 154 +++++++++++++++ .../danfoss_ally/pydanfossally/const.py | 3 + .../pydanfossally/danfossallyapi.py | 184 ++++++++++++++++++ custom_components/danfoss_ally/sensor.py | 10 +- 8 files changed, 430 insertions(+), 38 deletions(-) create mode 100644 custom_components/danfoss_ally/pydanfossally/__init__.py create mode 100644 custom_components/danfoss_ally/pydanfossally/const.py create mode 100644 custom_components/danfoss_ally/pydanfossally/danfossallyapi.py diff --git a/custom_components/danfoss_ally/__init__.py b/custom_components/danfoss_ally/__init__.py index 9a51585..23da10d 100644 --- a/custom_components/danfoss_ally/__init__.py +++ b/custom_components/danfoss_ally/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import Throttle -from pydanfossally import DanfossAlly +from .pydanfossally import DanfossAlly from .const import ( CONF_KEY, @@ -164,14 +164,28 @@ def update(self) -> None: _LOGGER.debug("%s: %s", device, self.ally.devices[device]) dispatcher_send(self.hass, SIGNAL_ALLY_UPDATE_RECEIVED) + def update_single_device(self, device_id, expected_mode) -> None: + """Update API data.""" + _LOGGER.debug("Updating Danfoss Ally device %s", device_id) + + # Update single device, and retry if we do not get the expected mode + itry = 0 + while itry == 0 or (itry <= 4 and expected_mode is not None and expected_mode != self.ally.devices[device_id]["mode"]): + #_LOGGER.debug("Retry: %s", itry) + itry += 1 + self.ally.getDevice(device_id) + + _LOGGER.debug("%s: %s", device_id, self.ally.devices[device_id]) + dispatcher_send(self.hass, SIGNAL_ALLY_UPDATE_RECEIVED) + @property def devices(self): """Return device list from API.""" return self.ally.devices - def set_temperature(self, device_id: str, temperature: float) -> None: + def set_temperature(self, device_id: str, temperature: float, code = "manual_mode_fast") -> None: """Set temperature for device_id.""" - self.ally.setTemperature(device_id, temperature) + self.ally.setTemperature(device_id, temperature, code) def set_mode(self, device_id: str, mode: str) -> None: """Set operating mode for device_id.""" diff --git a/custom_components/danfoss_ally/binary_sensor.py b/custom_components/danfoss_ally/binary_sensor.py index a4e8e5e..60fb8aa 100644 --- a/custom_components/danfoss_ally/binary_sensor.py +++ b/custom_components/danfoss_ally/binary_sensor.py @@ -66,7 +66,6 @@ async def async_setup_entry( ) ] ) - if entities: async_add_entities(entities, True) @@ -100,7 +99,7 @@ def __init__(self, ally, name, device_id, device_type): elif self._type == "open window": self._state = bool(self._device['window_open']) elif self._type == "child lock": - self._state = bool(self._device['child_lock']) + self._state = not bool(self._device['child_lock']) elif self._type == "connectivity": self._state = bool(self._device['online']) @@ -137,7 +136,7 @@ def device_class(self): return DEVICE_CLASS_CONNECTIVITY elif self._type == "open window": return DEVICE_CLASS_WINDOW - elif self._type == "child_lock": + elif self._type == "child lock": return DEVICE_CLASS_LOCK elif self._type == "connectivity": return DEVICE_CLASS_CONNECTIVITY @@ -163,6 +162,6 @@ def _async_update_data(self): elif self._type == "open window": self._state = bool(self._device['window_open']) elif self._type == "child lock": - self._state = bool(self._device['child_lock']) + self._state = not bool(self._device['child_lock']) elif self._type == "connectivity": - self._state = bool(self._device['online']) \ No newline at end of file + self._state = bool(self._device['online']) diff --git a/custom_components/danfoss_ally/climate.py b/custom_components/danfoss_ally/climate.py index e426549..5535f23 100644 --- a/custom_components/danfoss_ally/climate.py +++ b/custom_components/danfoss_ally/climate.py @@ -59,12 +59,7 @@ def __init__( self._unique_id = f"climate_{device_id}_ally" self._supported_hvac_modes = supported_hvac_modes - self._supported_preset_modes = [ - PRESET_HOME, - PRESET_AWAY, - PRESET_PAUSE, - PRESET_MANUAL, - ] + self._supported_preset_modes = [PRESET_HOME, PRESET_AWAY, PRESET_PAUSE, PRESET_MANUAL, "Holiday"] self._support_flags = support_flags self._available = False @@ -74,7 +69,7 @@ def __init__( self._cur_temp = self._device["temperature"] else: # TEMPORARY fix for missing temperature sensor - self._cur_temp = self._device["setpoint"] + self._cur_temp = self.get_setpoint_for_current_mode() #self._device["setpoint"] # Low temperature set in Ally app if "lower_temp" in self._device: @@ -125,7 +120,7 @@ def current_temperature(self): return self._device["temperature"] else: # TEMPORARY fix for missing temperature sensor - return self._device["setpoint"] + return self.get_setpoint_for_current_mode() #self._device["setpoint"] @property def hvac_mode(self): @@ -138,7 +133,7 @@ def hvac_mode(self): or self._device["mode"] == "leaving_home" ): return HVAC_MODE_AUTO - elif self._device["mode"] == "manual": + elif (self._device["mode"] == "manual" or self._device["mode"] == "pause" or self._device["mode"] == "holiday"): return HVAC_MODE_HEAT @property @@ -153,6 +148,8 @@ def preset_mode(self): return PRESET_PAUSE elif self._device["mode"] == "manual": return PRESET_MANUAL + elif self._device["mode"] == "holiday": + return "Holiday" @property def hvac_modes(self): @@ -179,12 +176,20 @@ def set_preset_mode(self, preset_mode): mode = "pause" elif preset_mode == PRESET_MANUAL: mode = "manual" + elif preset_mode == "Holiday": + mode = "holiday" if mode is None: return + self._device["mode"] = mode # Update current copy of device data + #self._ally.setMode(self._device_id, mode) self._ally.set_mode(self._device_id, mode) + # Update ASAP + self._ally.update_single_device(self._device_id, mode) + + @property def hvac_action(self): """Return the current running hvac operation if supported. @@ -209,16 +214,20 @@ def target_temperature_step(self): @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._device["setpoint"] + return self.get_setpoint_for_current_mode() #self._device["setpoint"] def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + setpoint_code = self.get_setpoint_code_for_mode(self._device["mode"]) + if temperature is None or setpoint_code is None: return - self._ally.set_mode(self._device_id, "manual") - self._ally.set_temperature(self._device_id, temperature) + self._device[setpoint_code] = temperature # Update temperature in current copy + self._ally.set_temperature(self._device_id, temperature, setpoint_code) + + # Update ASAP + self._ally.update_single_device(self._device_id, None) @property def available(self): @@ -262,6 +271,31 @@ def set_hvac_mode(self, hvac_mode): self._ally.set_mode(self._device_id, mode) + # Update ASAP + self._ally.update_single_device(self._device_id, mode) + + + def get_setpoint_code_for_mode(self, mode): + if mode == "at_home": + setpoint_code = "at_home_setting" + elif mode == "leaving_home": + setpoint_code = "leaving_home_setting" + elif mode == "pause": + setpoint_code = "pause_setting" + elif mode == "manual": + setpoint_code = "manual_mode_fast" + elif mode == "holiday": + setpoint_code = "holiday_setting" + return setpoint_code + + def get_setpoint_for_current_mode(self): + if "mode" in self._device: + setpoint_code = self.get_setpoint_code_for_mode(self._device["mode"]) + + if setpoint_code is not None and setpoint_code in self._device: + setpoint = self._device[setpoint_code] + + return(setpoint) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities diff --git a/custom_components/danfoss_ally/manifest.json b/custom_components/danfoss_ally/manifest.json index b9aba5b..58080fc 100644 --- a/custom_components/danfoss_ally/manifest.json +++ b/custom_components/danfoss_ally/manifest.json @@ -1,15 +1,15 @@ -{ - "domain": "danfoss_ally", - "name": "Danfoss Ally", - "documentation": "https://github.com/MTrab/danfoss_ally/blob/master/README.md", - "issue_tracker": "https://github.com/MTrab/danfoss_ally/issues", - "requirements": [ - "pydanfossally==0.0.26" - ], - "codeowners": [ - "@MTrab" - ], - "version": "1.0.7", - "config_flow": true, - "iot_class": "cloud_polling" +{ + "codeowners": [ + "@MTrab" + ], + "config_flow": true, + "documentation": "https://github.com/MTrab/danfoss_ally/blob/master/README.md", + "domain": "danfoss_ally", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/MTrab/danfoss_ally/issues", + "name": "Danfoss Ally", + "requirements": [ + "pydanfossally==0.0.26" + ], + "version": "v1.0.7" } \ No newline at end of file diff --git a/custom_components/danfoss_ally/pydanfossally/__init__.py b/custom_components/danfoss_ally/pydanfossally/__init__.py new file mode 100644 index 0000000..e016732 --- /dev/null +++ b/custom_components/danfoss_ally/pydanfossally/__init__.py @@ -0,0 +1,154 @@ +import logging + +from .danfossallyapi import * + +_LOGGER = logging.getLogger(__name__) + +__version__ = "0.0.26" + + +class DanfossAlly: + """Danfoss Ally API connector.""" + + def __init__(self): + """Init the API connector variables.""" + self._authorized = False + self._token = None + self.devices = {} + + self._api = DanfossAllyAPI() + + def initialize(self, key, secret): + """Authorize and initialize the connection.""" + + token = self._api.getToken(key, secret) + + if token is False: + self._authorized = False + _LOGGER.error("Error in authorization") + return False + + _LOGGER.debug("Token received: %s", self._api.token) + self._token = self._api.token + self._authorized = True + return self._authorized + + def getDeviceList(self): + """Get device list.""" + devices = self._api.get_devices() + + if devices is None: + _LOGGER.error("No devices loaded, API error?!") + return + + if not devices: + _LOGGER.error("No devices loaded, API connection error?!") + return + + if not "result" in devices: + _LOGGER.error("Something went wrong loading devices!") + return + + for device in devices["result"]: + self.handleDeviceDate(device) + + def handleDeviceDate(self, device): + self.devices[device["id"]] = {} + self.devices[device["id"]]["isThermostat"] = False + self.devices[device["id"]]["name"] = device["name"].strip() + self.devices[device["id"]]["online"] = device["online"] + self.devices[device["id"]]["update"] = device["update_time"] + if "model" in device: + self.devices[device["id"]]["model"] = device["model"] + + bHasFloorSensor = False + for status in device["status"]: + if status["code"] == "floor_sensor": + bHasFloorSensor = status["value"] + self.devices[device["id"]]["floor_sensor"] = bHasFloorSensor + + for status in device["status"]: + # if status["code"] == "temp_set": + # setpoint = float(status["value"]) + # setpoint = setpoint / 10 + # self.devices[device["id"]]["setpoint"] = setpoint + # self.devices[device["id"]]["isThermostat"] = True + if status["code"] in ["manual_mode_fast", "at_home_setting", "leaving_home_setting", "pause_setting", "holiday_setting"]: + setpoint = float(status["value"]) + setpoint = setpoint / 10 + self.devices[device["id"]][status["code"]] = setpoint + self.devices[device["id"]]["isThermostat"] = True + elif status["code"] == "temp_current": + temperature = float(status["value"]) + temperature = temperature / 10 + self.devices[device["id"]]["temperature"] = temperature + elif status["code"] == "MeasuredValue" and bHasFloorSensor: # Floor sensor + temperature = float(status["value"]) + temperature = temperature / 10 + self.devices[device["id"]]["floor temperature"] = temperature + elif status["code"] == "upper_temp": + temperature = float(status["value"]) + temperature = temperature / 10 + self.devices[device["id"]]["upper_temp"] = temperature + elif status["code"] == "lower_temp": + temperature = float(status["value"]) + temperature = temperature / 10 + self.devices[device["id"]]["lower_temp"] = temperature + elif status["code"] == "va_temperature": + temperature = float(status["value"]) + temperature = temperature / 10 + self.devices[device["id"]]["temperature"] = temperature + elif status["code"] == "va_humidity": + humidity = float(status["value"]) + humidity = humidity / 10 + self.devices[device["id"]]["humidity"] = humidity + elif status["code"] == "battery_percentage": + battery = status["value"] + self.devices[device["id"]]["battery"] = battery + elif status["code"] == "window_state": + window = status["value"] + if window == "open": + self.devices[device["id"]]["window_open"] = True + else: + self.devices[device["id"]]["window_open"] = False + elif status["code"] == "child_lock": + childlock = status["value"] + self.devices[device["id"]]["child_lock"] = childlock + elif status["code"] == "mode": + self.devices[device["id"]]["mode"] = status["value"] + elif status["code"] == "work_state": + self.devices[device["id"]]["work_state"] = status["value"] + + + def getDevice(self, device_id): + """Get device data.""" + device = self._api.get_device(device_id) + + if device is None or not device: + _LOGGER.error("No device loaded, API error?!") + return + if not "result" in device: + _LOGGER.error("Something went wrong loading devices!") + return + + self.handleDeviceDate(device["result"]) + + @property + def authorized(self): + """Return authorized status.""" + return self._authorized + + def setTemperature(self, device_id: str, temp: float, code = "manual_mode_fast") -> bool: + """Updates temperature setpoint for given device.""" + temperature = int(temp * 10) + + result = self._api.set_temperature(device_id, temperature, code) + + return result + + + def setMode(self, device_id: str, mode: str) -> bool: + """Updates operating mode for given device.""" + result = self._api.set_mode(device_id, mode) + + return result diff --git a/custom_components/danfoss_ally/pydanfossally/const.py b/custom_components/danfoss_ally/pydanfossally/const.py new file mode 100644 index 0000000..562a6eb --- /dev/null +++ b/custom_components/danfoss_ally/pydanfossally/const.py @@ -0,0 +1,3 @@ +THERMOSTAT_MODE_AUTO = "hot" +THERMOSTAT_MODE_MANUAL = "manual" +THERMOSTAT_MODE_OFF = "pause" diff --git a/custom_components/danfoss_ally/pydanfossally/danfossallyapi.py b/custom_components/danfoss_ally/pydanfossally/danfossallyapi.py new file mode 100644 index 0000000..a5cda86 --- /dev/null +++ b/custom_components/danfoss_ally/pydanfossally/danfossallyapi.py @@ -0,0 +1,184 @@ +import base64 +import datetime +import json +import logging + +import requests + +API_HOST = "https://api.danfoss.com" + +_LOGGER = logging.getLogger(__name__) + + +class DanfossAllyAPI: + def __init__(self): + """Init API.""" + self._key = "" + self._secret = "" + self._token = "" + self._refresh_at = datetime.datetime.now() + + def _call(self, path, headers_data, payload=None): + """Do the actual API call async.""" + + self._refresh_token() + try: + if payload: + req = requests.post( + API_HOST + path, json=payload, headers=headers_data, timeout=10 + ) + else: + req = requests.get(API_HOST + path, headers=headers_data, timeout=10) + + if not req.ok: + return False + except TimeoutError: + _LOGGER.warning("Timeout communication with Danfoss Ally API") + return False + except: + _LOGGER.warning( + "Unexpected error occured in communications with Danfoss Ally API!" + ) + return False + + return req.json() + + def _refresh_token(self): + """Refresh OAuth2 token if expired.""" + if self._refresh_at > datetime.datetime.now(): + return False + + self.getToken() + + def _generate_base64_token(self, key: str, secret: str) -> str: + """Generates a base64 token""" + key_secret = key + ":" + secret + key_secret_bytes = key_secret.encode("ascii") + base64_bytes = base64.b64encode(key_secret_bytes) + base64_token = base64_bytes.decode("ascii") + + return base64_token + + def getToken(self, key=None, secret=None) -> str: + """Get token.""" + + if not key is None: + self._key = key + if not secret is None: + self._secret = secret + + base64_token = self._generate_base64_token(self._key, self._secret) + + header_data = {} + header_data["Content-Type"] = "application/x-www-form-urlencoded" + header_data["Authorization"] = "Basic " + base64_token + header_data["Accept"] = "application/json" + + post_data = "grant_type=client_credentials" + try: + req = requests.post( + API_HOST + "/oauth2/token", + data=post_data, + headers=header_data, + timeout=10, + ) + + if not req.ok: + return False + except TimeoutError: + _LOGGER.warning("Timeout communication with Danfoss Ally API") + return False + except: + _LOGGER.warning( + "Unexpected error occured in communications with Danfoss Ally API!" + ) + return False + + callData = req.json() + + if callData is False: + return False + + expires_in = float(callData["expires_in"]) + self._refresh_at = datetime.datetime.now() + self._refresh_at = self._refresh_at + datetime.timedelta(seconds=expires_in) + self._refresh_at = self._refresh_at + datetime.timedelta(seconds=-30) + self._token = callData["access_token"] + return True + + def get_devices(self): + """Get list of all devices.""" + + header_data = {} + header_data["Accept"] = "application/json" + header_data["Authorization"] = "Bearer " + self._token + + callData = self._call("/ally/devices", header_data) + + return callData + + def get_device(self, device_id: str): + """Get device details.""" + + header_data = {} + header_data["Accept"] = "application/json" + header_data["Authorization"] = "Bearer " + self._token + + callData = self._call("/ally/devices/" + device_id, header_data) + + return callData + + def set_temperature(self, device_id: str, temp: int, code = "manual_mode_fast") -> bool: + """Set temperature setpoint.""" + + header_data = {} + header_data["Accept"] = "application/json" + header_data["Authorization"] = "Bearer " + self._token + + #request_body = {"commands": [{"code": "temp_set", "value": temp}]} + request_body = {"commands": [{"code": code, "value": temp}]} + + callData = self._call( + "/ally/devices/" + device_id + "/commands", header_data, request_body + ) + + _LOGGER.debug("Set temperature for device %s: %s", device_id, json.dumps(request_body)) + + return callData["result"] + + + + def set_mode(self, device_id: str, mode: str) -> bool: + """Set device operating mode.""" + + header_data = {} + header_data["Accept"] = "application/json" + header_data["Authorization"] = "Bearer " + self._token + + request_body = {"commands": [{"code": "mode", "value": mode}]} + + callData = self._call( + "/ally/devices/" + device_id + "/commands", header_data, request_body + ) + + return callData["result"] + + def set_mode(self, device_id, mode) -> bool: + """Set mode.""" + + header_data = {} + header_data["Accept"] = "application/json" + header_data["Authorization"] = "Bearer " + self._token + + request_body = {"commands": [{"code": "mode", "value": mode}]} + + callData = self._call( + "/ally/devices/" + device_id + "/commands", header_data, request_body + ) + + return callData["result"] + + @property + def token(self) -> str: + """Return token.""" + return self._token diff --git a/custom_components/danfoss_ally/sensor.py b/custom_components/danfoss_ally/sensor.py index d3b435d..8b49c38 100644 --- a/custom_components/danfoss_ally/sensor.py +++ b/custom_components/danfoss_ally/sensor.py @@ -31,7 +31,7 @@ async def async_setup_entry( entities = [] for device in ally.devices: - for sensor_type in ["battery", "temperature", "humidity"]: + for sensor_type in ["battery", "temperature", "humidity", "floor temperature"]: if sensor_type in ally.devices[device]: _LOGGER.debug( "Found %s sensor for %s", sensor_type, ally.devices[device]["name"] @@ -74,6 +74,8 @@ def __init__(self, ally, name, device_id, device_type): self._state = self._device["temperature"] elif self._type == "humidity": self._state = self._device["humidity"] + elif self._type == "floor temperature": + self._state = self._device["floor temperature"] async def async_added_to_hass(self): """Register for sensor updates.""" @@ -111,7 +113,7 @@ def unit_of_measurement(self): """Return the unit of measurement.""" if self._type == "battery": return PERCENTAGE - elif self._type == "temperature": + elif self._type == "temperature" or self._type == "floor temperature": return TEMP_CELSIUS elif self._type == "humidity": return PERCENTAGE @@ -122,7 +124,7 @@ def device_class(self): """Return the class of this sensor.""" if self._type == "battery": return DEVICE_CLASS_BATTERY - elif self._type == "temperature": + elif self._type == "temperature" or self._type == "floor temperature": return DEVICE_CLASS_TEMPERATURE elif self._type == "humidity": return DEVICE_CLASS_HUMIDITY @@ -146,3 +148,5 @@ def _async_update_data(self): self._state = self._device["temperature"] elif self._type == "humidity": self._state = self._device["humidity"] + elif self._type == "floor temperature": + self._state = self._device["floor temperature"] From 46b541bb95896f10bf310b104596b88a64ab0dd5 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 21 May 2022 17:07:08 +0200 Subject: [PATCH 02/18] Action and service added --- custom_components/danfoss_ally/__init__.py | 59 +++++++--- custom_components/danfoss_ally/climate.py | 88 ++++++++++---- custom_components/danfoss_ally/const.py | 7 ++ .../danfoss_ally/device_action.py | 110 ++++++++++++++++++ custom_components/danfoss_ally/sensor.py | 4 +- custom_components/danfoss_ally/services.yaml | 34 ++++++ custom_components/danfoss_ally/strings.json | 13 ++- .../danfoss_ally/translations/da.json | 11 ++ .../danfoss_ally/translations/en.json | 11 ++ 9 files changed, 293 insertions(+), 44 deletions(-) create mode 100644 custom_components/danfoss_ally/device_action.py create mode 100644 custom_components/danfoss_ally/services.yaml diff --git a/custom_components/danfoss_ally/__init__.py b/custom_components/danfoss_ally/__init__.py index 23da10d..4c590f4 100644 --- a/custom_components/danfoss_ally/__init__.py +++ b/custom_components/danfoss_ally/__init__.py @@ -1,6 +1,6 @@ """Adds support for Danfoss Ally Gateway.""" import asyncio -from datetime import timedelta +from datetime import datetime, timedelta import logging import voluptuous as vol @@ -89,11 +89,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Error authorizing") return False - await hass.async_add_executor_job(allyconnector.update) + async def _update(now): + """Periodic update.""" + await allyconnector.async_update() + await _update(None) #await hass.async_add_executor_job(allyconnector.update) update_track = async_track_time_interval( hass, - lambda now: allyconnector.update(), + _update, #lambda now: allyconnector.update(), timedelta(seconds=SCAN_INTERVAL), ) @@ -148,6 +151,8 @@ def __init__(self, hass, key, secret): self._secret = secret self.ally = DanfossAlly() self._authorized = False + self._latest_write_time = datetime.min + self._latest_poll_time = datetime.min def setup(self) -> None: """Setup API connection.""" @@ -156,27 +161,37 @@ def setup(self) -> None: self._authorized = auth @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self) -> None: + async def async_update(self) -> None: """Update API data.""" _LOGGER.debug("Updating Danfoss Ally devices") - self.ally.getDeviceList() + + # Postpone poll if a recent change were made - Attempt to avoid UI glitches + seconds_since_write = (datetime.utcnow() - self._latest_write_time).total_seconds() + if (seconds_since_write < 1): + _LOGGER.debug("Seconds since last write %f. Postponing update for 1 sec.", seconds_since_write) + await asyncio.sleep(1) + + # Poll API + await self.hass.async_add_executor_job(self.ally.getDeviceList) #self.ally.getDeviceList() + self._latest_poll_time = datetime.utcnow() + for device in self.ally.devices: # pylint: disable=consider-using-dict-items _LOGGER.debug("%s: %s", device, self.ally.devices[device]) dispatcher_send(self.hass, SIGNAL_ALLY_UPDATE_RECEIVED) - def update_single_device(self, device_id, expected_mode) -> None: - """Update API data.""" - _LOGGER.debug("Updating Danfoss Ally device %s", device_id) + # def update_single_device(self, device_id, expected_mode) -> None: + # """Update API data.""" + # _LOGGER.debug("Updating Danfoss Ally device %s", device_id) - # Update single device, and retry if we do not get the expected mode - itry = 0 - while itry == 0 or (itry <= 4 and expected_mode is not None and expected_mode != self.ally.devices[device_id]["mode"]): - #_LOGGER.debug("Retry: %s", itry) - itry += 1 - self.ally.getDevice(device_id) + # # Update single device, and retry if we do not get the expected mode + # itry = 0 + # while itry == 0 or (itry <= 4 and expected_mode is not None and expected_mode != self.ally.devices[device_id]["mode"]): + # #_LOGGER.debug("Retry: %s", itry) + # itry += 1 + # self.ally.getDevice(device_id) - _LOGGER.debug("%s: %s", device_id, self.ally.devices[device_id]) - dispatcher_send(self.hass, SIGNAL_ALLY_UPDATE_RECEIVED) + # _LOGGER.debug("%s: %s", device_id, self.ally.devices[device_id]) + # dispatcher_send(self.hass, SIGNAL_ALLY_UPDATE_RECEIVED) @property def devices(self): @@ -185,12 +200,24 @@ def devices(self): def set_temperature(self, device_id: str, temperature: float, code = "manual_mode_fast") -> None: """Set temperature for device_id.""" + self._latest_write_time = datetime.utcnow() self.ally.setTemperature(device_id, temperature, code) + # Debug info - log if update was done approximately as the same time as write + seconds_since_poll = (datetime.utcnow() - self._latest_poll_time).total_seconds() + if (seconds_since_poll < 0.5): + _LOGGER.warn("set_temperature: Time since last poll %f sec.", seconds_since_poll) + def set_mode(self, device_id: str, mode: str) -> None: """Set operating mode for device_id.""" + self._latest_write_time = datetime.utcnow() self.ally.setMode(device_id, mode) + # Debug info - log if update was done approximately as the same time as write + seconds_since_poll = (datetime.utcnow() - self._latest_poll_time).total_seconds() + if (seconds_since_poll < 0.5): + _LOGGER.warn("set_mode: Time since last poll %f sec.", seconds_since_poll) + @property def authorized(self) -> bool: """Return authorized state.""" diff --git a/custom_components/danfoss_ally/climate.py b/custom_components/danfoss_ally/climate.py index 5535f23..c6d56a9 100644 --- a/custom_components/danfoss_ally/climate.py +++ b/custom_components/danfoss_ally/climate.py @@ -1,6 +1,8 @@ """Support for Danfoss Ally thermostats.""" import logging +import voluptuous as vol + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, @@ -11,11 +13,15 @@ SUPPORT_PRESET_MODE, PRESET_HOME, PRESET_AWAY, + ATTR_PRESET_MODE, + ATTR_HVAC_MODE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_platform # config_validation as cv +import functools as ft from . import AllyConnector from .const import ( @@ -24,6 +30,7 @@ HVAC_MODE_MANUAL, PRESET_MANUAL, PRESET_PAUSE, + PRESET_HOLIDAY, SIGNAL_ALLY_UPDATE_RECEIVED, ) from .entity import AllyDeviceEntity @@ -59,7 +66,7 @@ def __init__( self._unique_id = f"climate_{device_id}_ally" self._supported_hvac_modes = supported_hvac_modes - self._supported_preset_modes = [PRESET_HOME, PRESET_AWAY, PRESET_PAUSE, PRESET_MANUAL, "Holiday"] + self._supported_preset_modes = [PRESET_HOME, PRESET_AWAY, PRESET_PAUSE, PRESET_MANUAL, PRESET_HOLIDAY] self._support_flags = support_flags self._available = False @@ -128,10 +135,7 @@ def hvac_mode(self): Need to be one of HVAC_MODE_*. """ if "mode" in self._device: - if ( - self._device["mode"] == "at_home" - or self._device["mode"] == "leaving_home" - ): + if (self._device["mode"] == "at_home" or self._device["mode"] == "leaving_home"): return HVAC_MODE_AUTO elif (self._device["mode"] == "manual" or self._device["mode"] == "pause" or self._device["mode"] == "holiday"): return HVAC_MODE_HEAT @@ -149,7 +153,7 @@ def preset_mode(self): elif self._device["mode"] == "manual": return PRESET_MANUAL elif self._device["mode"] == "holiday": - return "Holiday" + return PRESET_HOLIDAY @property def hvac_modes(self): @@ -176,7 +180,7 @@ def set_preset_mode(self, preset_mode): mode = "pause" elif preset_mode == PRESET_MANUAL: mode = "manual" - elif preset_mode == "Holiday": + elif preset_mode == PRESET_HOLIDAY: mode = "holiday" if mode is None: @@ -186,9 +190,8 @@ def set_preset_mode(self, preset_mode): #self._ally.setMode(self._device_id, mode) self._ally.set_mode(self._device_id, mode) - # Update ASAP - self._ally.update_single_device(self._device_id, mode) - + # Update UI + self.async_write_ha_state() @property def hvac_action(self): @@ -218,16 +221,39 @@ def target_temperature(self): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - setpoint_code = self.get_setpoint_code_for_mode(self._device["mode"]) - if temperature is None or setpoint_code is None: - return - - self._device[setpoint_code] = temperature # Update temperature in current copy - self._ally.set_temperature(self._device_id, temperature, setpoint_code) + #for key, value in kwargs.items(): + # _LOGGER.debug("%s = %s", key, value) + + if ATTR_TEMPERATURE in kwargs: + temperature = kwargs.get(ATTR_TEMPERATURE) + + if ATTR_PRESET_MODE in kwargs: + setpoint_code = self.get_setpoint_code_for_mode(kwargs.get(ATTR_PRESET_MODE)) # Preset_mode sent from action + elif ATTR_HVAC_MODE in kwargs: + value = kwargs.get(ATTR_HVAC_MODE) # HVAC_mode sent from action + if value == HVAC_MODE_AUTO: + setpoint_code = self.get_setpoint_code_for_mode("at_home") + if value == HVAC_MODE_HEAT: + setpoint_code = self.get_setpoint_code_for_mode("manual") + else: + setpoint_code = self.get_setpoint_code_for_mode(self._device["mode"]) # Current preset_mode + #_LOGGER.debug("setpoint_code: %s", setpoint_code) + + changed = False + if temperature is not None and setpoint_code is not None: + self._device[setpoint_code] = temperature # Update temperature in current copy + self._ally.set_temperature(self._device_id, temperature, setpoint_code) + changed = True + + # Update UI + if changed: + self.async_write_ha_state() + + async def set_preset_temperature(self, **kwargs): + await self.hass.async_add_executor_job( + ft.partial(self.set_temperature, **kwargs) + ) - # Update ASAP - self._ally.update_single_device(self._device_id, None) @property def available(self): @@ -269,16 +295,18 @@ def set_hvac_mode(self, hvac_mode): if mode is None: return + self._device["mode"] = mode # Update current copy of device data self._ally.set_mode(self._device_id, mode) - # Update ASAP - self._ally.update_single_device(self._device_id, mode) + # Update UI + self.async_write_ha_state() def get_setpoint_code_for_mode(self, mode): - if mode == "at_home": + setpoint_code = None + if mode == "at_home" or mode == "home": setpoint_code = "at_home_setting" - elif mode == "leaving_home": + elif mode == "leaving_home" or mode == "away": setpoint_code = "leaving_home_setting" elif mode == "pause": setpoint_code = "pause_setting" @@ -289,6 +317,7 @@ def get_setpoint_code_for_mode(self, mode): return setpoint_code def get_setpoint_for_current_mode(self): + setpoint = None if "mode" in self._device: setpoint_code = self.get_setpoint_code_for_mode(self._device["mode"]) @@ -302,9 +331,19 @@ async def async_setup_entry( ): """Set up the Danfoss Ally climate platform.""" + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + "set_preset_temperature", + { + vol.Required("temperature"): vol.Coerce(float), + vol.Optional("preset_mode"): str, + }, + "set_preset_temperature" + ) + ally: AllyConnector = hass.data[DOMAIN][entry.entry_id][DATA] entities = await hass.async_add_executor_job(_generate_entities, ally) - _LOGGER.debug(ally.devices) + #_LOGGER.debug(ally.devices) if entities: async_add_entities(entities, True) @@ -315,6 +354,7 @@ def _generate_entities(ally: AllyConnector): entities = [] for device in ally.devices: if ally.devices[device]["isThermostat"]: + _LOGGER.debug("Found climate entity for %s", ally.devices[device]["name"]) entity = create_climate_entity(ally, ally.devices[device]["name"], device) if entity: entities.append(entity) diff --git a/custom_components/danfoss_ally/const.py b/custom_components/danfoss_ally/const.py index 156c116..3679417 100644 --- a/custom_components/danfoss_ally/const.py +++ b/custom_components/danfoss_ally/const.py @@ -13,6 +13,7 @@ PRESET_MANUAL = "Manual" PRESET_PAUSE = "Pause" +PRESET_HOLIDAY = "Holiday" HA_TO_DANFOSS_HVAC_MODE_MAP = { HVAC_MODE_OFF: THERMOSTAT_MODE_OFF, @@ -33,3 +34,9 @@ UNIQUE_ID = "unique_id" UPDATE_LISTENER = "update_listener" UPDATE_TRACK = "update_track" + +ACTION_TYPE_SET_PRESET_TEMPERATURE = "set_preset_temperature" +ATTR_SETPOINT = "setpoint" + + + diff --git a/custom_components/danfoss_ally/device_action.py b/custom_components/danfoss_ally/device_action.py new file mode 100644 index 0000000..e5c112c --- /dev/null +++ b/custom_components/danfoss_ally/device_action.py @@ -0,0 +1,110 @@ +"""Provides device automations for Climate.""" +from __future__ import annotations + +import logging +import voluptuous as vol +import json + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, + ATTR_TEMPERATURE, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import get_capability, get_supported_features +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, ATTR_PRESET_MODE, ATTR_PRESET_MODES + +from .const import DOMAIN, ACTION_TYPE_SET_PRESET_TEMPERATURE, ATTR_SETPOINT + +_LOGGER = logging.getLogger(__name__) + +ACTION_TYPES = {ACTION_TYPE_SET_PRESET_TEMPERATURE} + +SET_SETPOINT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): ACTION_TYPE_SET_PRESET_TEMPERATURE, + vol.Required(CONF_ENTITY_ID): cv.entity_domain(CLIMATE_DOMAIN), + vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), + vol.Optional(ATTR_PRESET_MODE): str, + } +) + +ACTION_SCHEMA = vol.Any(SET_SETPOINT_SCHEMA) + + +async def async_get_actions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device actions for Climate devices.""" + registry = entity_registry.async_get(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != CLIMATE_DOMAIN: + continue + + supported_features = get_supported_features(hass, entry.entity_id) + + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + _LOGGER.debug("Action: " + json.dumps({**base_action, CONF_TYPE: ACTION_TYPE_SET_PRESET_TEMPERATURE})) + + actions.append({**base_action, CONF_TYPE: ACTION_TYPE_SET_PRESET_TEMPERATURE}) + # if supported_features & const.SUPPORT_PRESET_MODE: + # actions.append({**base_action, CONF_TYPE: "set_preset_mode"}) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Context | None +) -> None: + """Execute a device action.""" + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == ACTION_TYPE_SET_PRESET_TEMPERATURE: + service = ACTION_TYPE_SET_PRESET_TEMPERATURE + service_data[ATTR_TEMPERATURE] = config[ATTR_TEMPERATURE] + if ATTR_PRESET_MODE in config: + service_data[ATTR_PRESET_MODE] = config[ATTR_PRESET_MODE] + domain = DOMAIN # danfoss_ally + + await hass.services.async_call( + domain, service, service_data, blocking=True, context=context + ) + + +async def async_get_action_capabilities(hass, config): + """List action capabilities.""" + action_type = config[CONF_TYPE] + + fields = {} + + if action_type == ACTION_TYPE_SET_PRESET_TEMPERATURE: + try: + preset_modes = ( + get_capability(hass, config[ATTR_ENTITY_ID], ATTR_PRESET_MODES) + or [] + ) + except HomeAssistantError: + preset_modes = [] + + preset_modes_kv = {} + for entry in preset_modes: + preset_modes_kv[entry.lower()] = entry.capitalize() + + fields[vol.Required(ATTR_TEMPERATURE)] = vol.Coerce(float) + fields[vol.Optional(ATTR_PRESET_MODE)] = vol.In(preset_modes_kv) + + return {"extra_fields": vol.Schema(fields)} diff --git a/custom_components/danfoss_ally/sensor.py b/custom_components/danfoss_ally/sensor.py index 8b49c38..927ab99 100644 --- a/custom_components/danfoss_ally/sensor.py +++ b/custom_components/danfoss_ally/sensor.py @@ -33,9 +33,7 @@ async def async_setup_entry( for device in ally.devices: for sensor_type in ["battery", "temperature", "humidity", "floor temperature"]: if sensor_type in ally.devices[device]: - _LOGGER.debug( - "Found %s sensor for %s", sensor_type, ally.devices[device]["name"] - ) + _LOGGER.debug("Found %s sensor for %s", sensor_type, ally.devices[device]["name"]) entities.extend( [ AllySensor( diff --git a/custom_components/danfoss_ally/services.yaml b/custom_components/danfoss_ally/services.yaml new file mode 100644 index 0000000..6d54e63 --- /dev/null +++ b/custom_components/danfoss_ally/services.yaml @@ -0,0 +1,34 @@ +# Describes the format for available climate services + +set_preset_temperature: + name: Set preset temperature + description: Set target temperature of climate device. + target: + entity: + domain: climate + fields: + temperature: + name: Temperature + description: New target temperature for HVAC. + selector: + number: + min: 0 + max: 250 + step: 0.5 + mode: box + preset_mode: + name: Preset mode + description: Optional. Using current preset mode if not specified. + selector: + select: + options: + - label: "Home" + value: "home" + - label: "Away" + value: "away" + - label: "Manual" + value: "manual" + - label: "Pause" + value: "pause" + - label: "Holiday" + value: "holiday" diff --git a/custom_components/danfoss_ally/strings.json b/custom_components/danfoss_ally/strings.json index 5172678..ef7485c 100644 --- a/custom_components/danfoss_ally/strings.json +++ b/custom_components/danfoss_ally/strings.json @@ -19,5 +19,16 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + + "device_automation": { + "condition_type": { + }, + "trigger_type": { + }, + "action_type": { + "set_preset_temperature": "Set preset temperature" + } } - } \ No newline at end of file + +} diff --git a/custom_components/danfoss_ally/translations/da.json b/custom_components/danfoss_ally/translations/da.json index f6efe35..4e69e31 100644 --- a/custom_components/danfoss_ally/translations/da.json +++ b/custom_components/danfoss_ally/translations/da.json @@ -16,5 +16,16 @@ } } } + }, + + "device_automation": { + "condition_type": { + }, + "trigger_type": { + }, + "action_type": { + "set_preset_temperature": "Set preset temperature" + } } + } \ No newline at end of file diff --git a/custom_components/danfoss_ally/translations/en.json b/custom_components/danfoss_ally/translations/en.json index b211d5b..2249075 100644 --- a/custom_components/danfoss_ally/translations/en.json +++ b/custom_components/danfoss_ally/translations/en.json @@ -16,5 +16,16 @@ } } } + }, + + "device_automation": { + "condition_type": { + }, + "trigger_type": { + }, + "action_type": { + "set_preset_temperature": "Set preset temperature" + } } + } \ No newline at end of file From 95de08de9abb83a8e622d6c506e7781225f677cc Mon Sep 17 00:00:00 2001 From: Jan <20459196+jnxxx@users.noreply.github.com> Date: Sat, 21 May 2022 17:51:50 +0200 Subject: [PATCH 03/18] Update README.md --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index 3a5fce4..36f0d51 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,47 @@ +# Danfoss Ally + + +This integration is forked from: **[MTrab / danfoss_ally](https://github.com/MTrab/danfoss_ally)** v1.0.7 + +### Improvements + +- Reading and writing setpoints using: `manual_mode_fast`, `at_home_setting`, `leaving_home_setting`, `pause_setting`, `holiday_setting` depending on the preset mode, rather than using `temp_set` as before. +It seems to work, so far. + +- Holiday preset mode added. + +- Quicker reaction to changes done in the UI + +- Added floor temperature sensor + +- Fix for setmode issue + +- Added action and service call to set target temperature for a specific preset mode. +Preset mode is optional, and writes to current preset mode when not specified. + +##### Things to note in the Danfoss Ally app + +- The app shows the floor temperature (when present) in the overview, and the room temperature on the details page. That is somewhat confusing, I think. Especially when it doesn't indicate it which is which. + +- When switching to manual mode from the app, it will take the previous taget temparature as target also for manual. Thus, overwrite target temperature for manual preset. +Switching from this integration will just switch to manual and not overwrite target temperature, unless specifically set. + +*Note: Changes are not tested with radiator thermostates* +
+
+
+
+
+
+
+
+ +--- +Previous README + +--- +
+ [![](https://img.shields.io/github/release/mtrab/danfoss_ally/all.svg?style=plastic)](https://github.com/mtrab/danfoss_ally/releases) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=plastic)](https://github.com/custom-components/hacs) From 0dd0dbb7455cf9d15b1f047df7d34f59bf4ebd59 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 22 May 2022 15:12:30 +0200 Subject: [PATCH 04/18] Banner control added --- .../danfoss_ally/binary_sensor.py | 36 ++++++++++++++++--- custom_components/danfoss_ally/climate.py | 17 +++++---- custom_components/danfoss_ally/entity.py | 5 +-- .../danfoss_ally/pydanfossally/__init__.py | 19 +++++----- custom_components/danfoss_ally/sensor.py | 6 ++-- 5 files changed, 59 insertions(+), 24 deletions(-) diff --git a/custom_components/danfoss_ally/binary_sensor.py b/custom_components/danfoss_ally/binary_sensor.py index 60fb8aa..f6e3493 100644 --- a/custom_components/danfoss_ally/binary_sensor.py +++ b/custom_components/danfoss_ally/binary_sensor.py @@ -5,6 +5,7 @@ DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_WINDOW, DEVICE_CLASS_LOCK, + DEVICE_CLASS_TAMPER, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -38,7 +39,8 @@ async def async_setup_entry( ally, ally.devices[device]["name"], device, - 'open window' + 'open window', + ally.devices[device]["model"] ) ] ) @@ -50,7 +52,8 @@ async def async_setup_entry( ally, ally.devices[device]["name"], device, - 'child lock' + 'child lock', + ally.devices[device]["model"] ) ] ) @@ -62,11 +65,27 @@ async def async_setup_entry( ally, ally.devices[device]["name"], device, - 'connectivity' + 'connectivity', + ally.devices[device]["model"] + ) + ] + ) + if 'banner_ctrl' in ally.devices[device]: + _LOGGER.debug("Found banner_ctrl detector for %s", ally.devices[device]["name"]) + entities.extend( + [ + AllyBinarySensor( + ally, + ally.devices[device]["name"], + device, + 'banner control', + ally.devices[device]["model"] ) ] ) + + if entities: async_add_entities(entities, True) @@ -74,13 +93,13 @@ async def async_setup_entry( class AllyBinarySensor(AllyDeviceEntity, BinarySensorEntity): """Representation of an Ally binary_sensor.""" - def __init__(self, ally, name, device_id, device_type): + def __init__(self, ally, name, device_id, device_type, model): """Initialize Ally binary_sensor.""" self._ally = ally self._device = ally.devices[device_id] self._device_id = device_id self._type = device_type - super().__init__(name, device_id, device_type) + super().__init__(name, device_id, device_type, model) _LOGGER.debug( "Device_id: %s --- Device: %s", @@ -102,6 +121,9 @@ def __init__(self, ally, name, device_id, device_type): self._state = not bool(self._device['child_lock']) elif self._type == "connectivity": self._state = bool(self._device['online']) + elif self._type == "banner control": + self._state = bool(self._device['banner_ctrl']) + async def async_added_to_hass(self): """Register for sensor updates.""" @@ -140,6 +162,8 @@ def device_class(self): return DEVICE_CLASS_LOCK elif self._type == "connectivity": return DEVICE_CLASS_CONNECTIVITY + elif self._type == "banner control": + return DEVICE_CLASS_TAMPER return None @callback @@ -165,3 +189,5 @@ def _async_update_data(self): self._state = not bool(self._device['child_lock']) elif self._type == "connectivity": self._state = bool(self._device['online']) + elif self._type == "banner control": + self._state = bool(self._device['banner_ctrl']) diff --git a/custom_components/danfoss_ally/climate.py b/custom_components/danfoss_ally/climate.py index c6d56a9..8dac52e 100644 --- a/custom_components/danfoss_ally/climate.py +++ b/custom_components/danfoss_ally/climate.py @@ -49,6 +49,7 @@ def __init__( ally, name, device_id, + model, heat_min_temp, heat_max_temp, heat_step, @@ -59,7 +60,7 @@ def __init__( self._ally = ally self._device = ally.devices[device_id] self._device_id = device_id - super().__init__(name, device_id, "climate") + super().__init__(name, device_id, "climate", model) _LOGGER.debug("Device_id: %s --- Device: %s", self._device_id, self._device) @@ -302,9 +303,12 @@ def set_hvac_mode(self, hvac_mode): self.async_write_ha_state() - def get_setpoint_code_for_mode(self, mode): + def get_setpoint_code_for_mode(self, mode, for_writing = True): setpoint_code = None - if mode == "at_home" or mode == "home": + if for_writing == False and "banner_ctrl" in self._device and bool(self._device['banner_ctrl']): + # Temperature setpoint is overridden locally at the thermostate + setpoint_code = "manual_mode_fast" + elif mode == "at_home" or mode == "home": setpoint_code = "at_home_setting" elif mode == "leaving_home" or mode == "away": setpoint_code = "leaving_home_setting" @@ -319,7 +323,7 @@ def get_setpoint_code_for_mode(self, mode): def get_setpoint_for_current_mode(self): setpoint = None if "mode" in self._device: - setpoint_code = self.get_setpoint_code_for_mode(self._device["mode"]) + setpoint_code = self.get_setpoint_code_for_mode(self._device["mode"], False) if setpoint_code is not None and setpoint_code in self._device: setpoint = self._device[setpoint_code] @@ -355,13 +359,13 @@ def _generate_entities(ally: AllyConnector): for device in ally.devices: if ally.devices[device]["isThermostat"]: _LOGGER.debug("Found climate entity for %s", ally.devices[device]["name"]) - entity = create_climate_entity(ally, ally.devices[device]["name"], device) + entity = create_climate_entity(ally, ally.devices[device]["name"], device, ally.devices[device]["model"]) if entity: entities.append(entity) return entities -def create_climate_entity(ally, name: str, device_id: str) -> AllyClimate: +def create_climate_entity(ally, name: str, device_id: str, model: str) -> AllyClimate: """Create a Danfoss Ally climate entity.""" support_flags = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -374,6 +378,7 @@ def create_climate_entity(ally, name: str, device_id: str) -> AllyClimate: ally, name, device_id, + model, heat_min_temp, heat_max_temp, heat_step, diff --git a/custom_components/danfoss_ally/entity.py b/custom_components/danfoss_ally/entity.py index 26dce19..639b7c9 100644 --- a/custom_components/danfoss_ally/entity.py +++ b/custom_components/danfoss_ally/entity.py @@ -7,12 +7,13 @@ class AllyDeviceEntity(Entity): """Base implementation for Ally device.""" - def __init__(self, name, device_id, device_type): + def __init__(self, name, device_id, device_type, model = None): """Initialize a Ally device.""" super().__init__() self._type = device_type self._name = name self._device_id = device_id + self._model = model @property def device_info(self): @@ -21,7 +22,7 @@ def device_info(self): "identifiers": {(DOMAIN, self._device_id)}, "name": self._name, "manufacturer": DEFAULT_NAME, - "model": None, + "model": self._model, } @property diff --git a/custom_components/danfoss_ally/pydanfossally/__init__.py b/custom_components/danfoss_ally/pydanfossally/__init__.py index e016732..280a4c6 100644 --- a/custom_components/danfoss_ally/pydanfossally/__init__.py +++ b/custom_components/danfoss_ally/pydanfossally/__init__.py @@ -60,6 +60,8 @@ def handleDeviceDate(self, device): self.devices[device["id"]]["update"] = device["update_time"] if "model" in device: self.devices[device["id"]]["model"] = device["model"] + elif "device_type" in device: + self.devices[device["id"]]["model"] = device["device_type"] bHasFloorSensor = False for status in device["status"]: @@ -111,14 +113,15 @@ def handleDeviceDate(self, device): self.devices[device["id"]]["window_open"] = True else: self.devices[device["id"]]["window_open"] = False - elif status["code"] == "child_lock": - childlock = status["value"] - self.devices[device["id"]]["child_lock"] = childlock - elif status["code"] == "mode": - self.devices[device["id"]]["mode"] = status["value"] - elif status["code"] == "work_state": - self.devices[device["id"]]["work_state"] = status["value"] - + # elif status["code"] == "child_lock": + # childlock = status["value"] + # self.devices[device["id"]]["child_lock"] = childlock + # elif status["code"] == "mode": + # self.devices[device["id"]]["mode"] = status["value"] + # elif status["code"] == "work_state": + # self.devices[device["id"]]["work_state"] = status["value"] + if status["code"] in ["child_lock", "mode", "work_state", "banner_ctrl"]: + self.devices[device["id"]][status["code"]] = status["value"] def getDevice(self, device_id): """Get device data.""" diff --git a/custom_components/danfoss_ally/sensor.py b/custom_components/danfoss_ally/sensor.py index 927ab99..089c787 100644 --- a/custom_components/danfoss_ally/sensor.py +++ b/custom_components/danfoss_ally/sensor.py @@ -37,7 +37,7 @@ async def async_setup_entry( entities.extend( [ AllySensor( - ally, ally.devices[device]["name"], device, sensor_type + ally, ally.devices[device]["name"], device, sensor_type, ally.devices[device]["model"] ) ] ) @@ -49,13 +49,13 @@ async def async_setup_entry( class AllySensor(AllyDeviceEntity): """Representation of an Ally sensor.""" - def __init__(self, ally, name, device_id, device_type): + def __init__(self, ally, name, device_id, device_type, model): """Initialize Ally binary_sensor.""" self._ally = ally self._device = ally.devices[device_id] self._device_id = device_id self._type = device_type - super().__init__(name, device_id, device_type) + super().__init__(name, device_id, device_type, model) _LOGGER.debug("Device_id: %s --- Device: %s", self._device_id, self._device) From 98c6ef8f8aa8422738a8bc55278fe0313f786ba2 Mon Sep 17 00:00:00 2001 From: Jan <20459196+jnxxx@users.noreply.github.com> Date: Sun, 22 May 2022 15:26:18 +0200 Subject: [PATCH 05/18] Update README.md --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 36f0d51..61b0ee8 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,26 @@ This integration is forked from: **[MTrab / danfoss_ally](https://github.com/MTrab/danfoss_ally)** v1.0.7 -### Improvements +### Additions - Reading and writing setpoints using: `manual_mode_fast`, `at_home_setting`, `leaving_home_setting`, `pause_setting`, `holiday_setting` depending on the preset mode, rather than using `temp_set` as before. It seems to work, so far. - Holiday preset mode added. -- Quicker reaction to changes done in the UI +- Quicker reaction to changes performed in the UI - Added floor temperature sensor - Fix for setmode issue -- Added action and service call to set target temperature for a specific preset mode. +- Added action and service call to set target temperature for a specific preset mode. Preset mode is optional, and writes to current preset mode when not specified. +- Added an indication for 'banner control' (local override). +When setpoint is changed locally from the thermostate it raises this flag and uses this as manual target setpoint. + + ##### Things to note in the Danfoss Ally app - The app shows the floor temperature (when present) in the overview, and the room temperature on the details page. That is somewhat confusing, I think. Especially when it doesn't indicate it which is which. From 07973c75090733a10a1bb49191c9d561666aeca2 Mon Sep 17 00:00:00 2001 From: Jan <20459196+jnxxx@users.noreply.github.com> Date: Sun, 29 May 2022 14:16:33 +0200 Subject: [PATCH 06/18] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 61b0ee8..305c778 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ When setpoint is changed locally from the thermostate it raises this flag and us - When switching to manual mode from the app, it will take the previous taget temparature as target also for manual. Thus, overwrite target temperature for manual preset. Switching from this integration will just switch to manual and not overwrite target temperature, unless specifically set. -*Note: Changes are not tested with radiator thermostates* +*Note: Changes are limited tested with radiator thermostates*


From 2eda3352ecd97ac6ce117e011d9ac60efd7e3b05 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 29 Jul 2022 16:50:13 +0200 Subject: [PATCH 07/18] Prepare for pr --- custom_components/danfoss_ally/__init__.py | 20 +- .../danfoss_ally/binary_sensor.py | 77 +++----- custom_components/danfoss_ally/climate.py | 25 ++- custom_components/danfoss_ally/manifest.json | 18 +- .../danfoss_ally/pydanfossally/__init__.py | 157 --------------- .../danfoss_ally/pydanfossally/const.py | 3 - .../pydanfossally/danfossallyapi.py | 184 ------------------ custom_components/danfoss_ally/sensor.py | 14 +- 8 files changed, 61 insertions(+), 437 deletions(-) delete mode 100644 custom_components/danfoss_ally/pydanfossally/__init__.py delete mode 100644 custom_components/danfoss_ally/pydanfossally/const.py delete mode 100644 custom_components/danfoss_ally/pydanfossally/danfossallyapi.py diff --git a/custom_components/danfoss_ally/__init__.py b/custom_components/danfoss_ally/__init__.py index 4c590f4..3efa539 100644 --- a/custom_components/danfoss_ally/__init__.py +++ b/custom_components/danfoss_ally/__init__.py @@ -1,9 +1,9 @@ """Adds support for Danfoss Ally Gateway.""" import asyncio -from datetime import datetime, timedelta import logging -import voluptuous as vol +from datetime import datetime, timedelta +import voluptuous as vol from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -12,7 +12,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import Throttle -from .pydanfossally import DanfossAlly +from pydanfossally import DanfossAlly from .const import ( CONF_KEY, @@ -179,20 +179,6 @@ async def async_update(self) -> None: _LOGGER.debug("%s: %s", device, self.ally.devices[device]) dispatcher_send(self.hass, SIGNAL_ALLY_UPDATE_RECEIVED) - # def update_single_device(self, device_id, expected_mode) -> None: - # """Update API data.""" - # _LOGGER.debug("Updating Danfoss Ally device %s", device_id) - - # # Update single device, and retry if we do not get the expected mode - # itry = 0 - # while itry == 0 or (itry <= 4 and expected_mode is not None and expected_mode != self.ally.devices[device_id]["mode"]): - # #_LOGGER.debug("Retry: %s", itry) - # itry += 1 - # self.ally.getDevice(device_id) - - # _LOGGER.debug("%s: %s", device_id, self.ally.devices[device_id]) - # dispatcher_send(self.hass, SIGNAL_ALLY_UPDATE_RECEIVED) - @property def devices(self): """Return device list from API.""" diff --git a/custom_components/danfoss_ally/binary_sensor.py b/custom_components/danfoss_ally/binary_sensor.py index f6e3493..3ef9245 100644 --- a/custom_components/danfoss_ally/binary_sensor.py +++ b/custom_components/danfoss_ally/binary_sensor.py @@ -3,8 +3,8 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_WINDOW, DEVICE_CLASS_LOCK, + DEVICE_CLASS_WINDOW, DEVICE_CLASS_TAMPER, BinarySensorEntity, ) @@ -12,11 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ( - DATA, - DOMAIN, - SIGNAL_ALLY_UPDATE_RECEIVED, -) +from .const import DATA, DOMAIN, SIGNAL_ALLY_UPDATE_RECEIVED from .entity import AllyDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -31,55 +27,45 @@ async def async_setup_entry( entities = [] for device in ally.devices: - if 'window_open' in ally.devices[device]: + if "window_open" in ally.devices[device]: _LOGGER.debug("Found window detector for %s", ally.devices[device]["name"]) entities.extend( [ AllyBinarySensor( - ally, - ally.devices[device]["name"], - device, - 'open window', - ally.devices[device]["model"] + ally, ally.devices[device]["name"], device, "open window", ally.devices[device]["model"] ) ] ) - if 'child_lock' in ally.devices[device]: - _LOGGER.debug("Found child lock detector for %s", ally.devices[device]["name"]) + if "child_lock" in ally.devices[device]: + _LOGGER.debug( + "Found child lock detector for %s", ally.devices[device]["name"] + ) entities.extend( [ AllyBinarySensor( - ally, - ally.devices[device]["name"], - device, - 'child lock', - ally.devices[device]["model"] + ally, ally.devices[device]["name"], device, "child lock", ally.devices[device]["model"] ) ] ) if not ally.devices[device]["isThermostat"]: - _LOGGER.debug("Found connection sensor for %s", ally.devices[device]["name"]) + _LOGGER.debug( + "Found connection sensor for %s", ally.devices[device]["name"] + ) entities.extend( [ AllyBinarySensor( - ally, - ally.devices[device]["name"], - device, - 'connectivity', - ally.devices[device]["model"] + ally, ally.devices[device]["name"], device, "connectivity", ally.devices[device]["model"] ) ] ) if 'banner_ctrl' in ally.devices[device]: - _LOGGER.debug("Found banner_ctrl detector for %s", ally.devices[device]["name"]) + _LOGGER.debug( + "Found banner_ctrl detector for %s", ally.devices[device]["name"] + ) entities.extend( [ AllyBinarySensor( - ally, - ally.devices[device]["name"], - device, - 'banner control', - ally.devices[device]["model"] + ally, ally.devices[device]["name"], device, 'banner control', ally.devices[device]["model"] ) ] ) @@ -101,11 +87,7 @@ def __init__(self, ally, name, device_id, device_type, model): self._type = device_type super().__init__(name, device_id, device_type, model) - _LOGGER.debug( - "Device_id: %s --- Device: %s", - self._device_id, - self._device - ) + _LOGGER.debug("Device_id: %s --- Device: %s", self._device_id, self._device) self._type = device_type @@ -114,15 +96,15 @@ def __init__(self, ally, name, device_id, device_type, model): self._state = None if self._type == "link": - self._state = self._device['online'] + self._state = self._device["online"] elif self._type == "open window": - self._state = bool(self._device['window_open']) + self._state = bool(self._device["window_open"]) elif self._type == "child lock": - self._state = not bool(self._device['child_lock']) + self._state = not bool(self._device["child_lock"]) elif self._type == "connectivity": - self._state = bool(self._device['online']) + self._state = bool(self._device["online"]) elif self._type == "banner control": - self._state = bool(self._device['banner_ctrl']) + self._state = bool(self._device["banner_ctrl"]) async def async_added_to_hass(self): @@ -175,19 +157,16 @@ def _async_update_callback(self): @callback def _async_update_data(self): """Load data.""" - _LOGGER.debug( - "Loading new binary_sensor data for device %s", - self._device_id - ) + _LOGGER.debug("Loading new binary_sensor data for device %s", self._device_id) self._device = self._ally.devices[self._device_id] if self._type == "link": - self._state = self._device['online'] + self._state = self._device["online"] elif self._type == "open window": - self._state = bool(self._device['window_open']) + self._state = bool(self._device["window_open"]) elif self._type == "child lock": - self._state = not bool(self._device['child_lock']) + self._state = not bool(self._device["child_lock"]) elif self._type == "connectivity": - self._state = bool(self._device['online']) + self._state = bool(self._device["online"]) elif self._type == "banner control": self._state = bool(self._device['banner_ctrl']) diff --git a/custom_components/danfoss_ally/climate.py b/custom_components/danfoss_ally/climate.py index 8dac52e..e4fc358 100644 --- a/custom_components/danfoss_ally/climate.py +++ b/custom_components/danfoss_ally/climate.py @@ -2,17 +2,16 @@ import logging import voluptuous as vol - from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_HEAT, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_PRESET_MODE, - PRESET_HOME, PRESET_AWAY, + PRESET_HOME, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ATTR_PRESET_MODE, ATTR_HVAC_MODE, ) @@ -20,7 +19,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers import entity_platform # config_validation as cv +from homeassistant.helpers import entity_platform import functools as ft from . import AllyConnector @@ -67,7 +66,13 @@ def __init__( self._unique_id = f"climate_{device_id}_ally" self._supported_hvac_modes = supported_hvac_modes - self._supported_preset_modes = [PRESET_HOME, PRESET_AWAY, PRESET_PAUSE, PRESET_MANUAL, PRESET_HOLIDAY] + self._supported_preset_modes = [ + PRESET_HOME, + PRESET_AWAY, + PRESET_PAUSE, + PRESET_MANUAL, + PRESET_HOLIDAY + ] self._support_flags = support_flags self._available = False @@ -136,7 +141,10 @@ def hvac_mode(self): Need to be one of HVAC_MODE_*. """ if "mode" in self._device: - if (self._device["mode"] == "at_home" or self._device["mode"] == "leaving_home"): + if ( + self._device["mode"] == "at_home" + or self._device["mode"] == "leaving_home" + ): return HVAC_MODE_AUTO elif (self._device["mode"] == "manual" or self._device["mode"] == "pause" or self._device["mode"] == "holiday"): return HVAC_MODE_HEAT @@ -188,7 +196,6 @@ def set_preset_mode(self, preset_mode): return self._device["mode"] = mode # Update current copy of device data - #self._ally.setMode(self._device_id, mode) self._ally.set_mode(self._device_id, mode) # Update UI @@ -222,8 +229,6 @@ def target_temperature(self): def set_temperature(self, **kwargs): """Set new target temperature.""" - #for key, value in kwargs.items(): - # _LOGGER.debug("%s = %s", key, value) if ATTR_TEMPERATURE in kwargs: temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/custom_components/danfoss_ally/manifest.json b/custom_components/danfoss_ally/manifest.json index 58080fc..b7b98ae 100644 --- a/custom_components/danfoss_ally/manifest.json +++ b/custom_components/danfoss_ally/manifest.json @@ -1,15 +1,15 @@ { - "codeowners": [ - "@MTrab" - ], - "config_flow": true, - "documentation": "https://github.com/MTrab/danfoss_ally/blob/master/README.md", "domain": "danfoss_ally", - "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/MTrab/danfoss_ally/issues", "name": "Danfoss Ally", + "documentation": "https://github.com/MTrab/danfoss_ally/blob/master/README.md", + "issue_tracker": "https://github.com/MTrab/danfoss_ally/issues", "requirements": [ - "pydanfossally==0.0.26" + "pydanfossally==0.0.27" + ], + "codeowners": [ + "@MTrab" ], - "version": "v1.0.7" + "version": "1.0.7", + "config_flow": true, + "iot_class": "cloud_polling" } \ No newline at end of file diff --git a/custom_components/danfoss_ally/pydanfossally/__init__.py b/custom_components/danfoss_ally/pydanfossally/__init__.py deleted file mode 100644 index 280a4c6..0000000 --- a/custom_components/danfoss_ally/pydanfossally/__init__.py +++ /dev/null @@ -1,157 +0,0 @@ -import logging - -from .danfossallyapi import * - -_LOGGER = logging.getLogger(__name__) - -__version__ = "0.0.26" - - -class DanfossAlly: - """Danfoss Ally API connector.""" - - def __init__(self): - """Init the API connector variables.""" - self._authorized = False - self._token = None - self.devices = {} - - self._api = DanfossAllyAPI() - - def initialize(self, key, secret): - """Authorize and initialize the connection.""" - - token = self._api.getToken(key, secret) - - if token is False: - self._authorized = False - _LOGGER.error("Error in authorization") - return False - - _LOGGER.debug("Token received: %s", self._api.token) - self._token = self._api.token - self._authorized = True - return self._authorized - - def getDeviceList(self): - """Get device list.""" - devices = self._api.get_devices() - - if devices is None: - _LOGGER.error("No devices loaded, API error?!") - return - - if not devices: - _LOGGER.error("No devices loaded, API connection error?!") - return - - if not "result" in devices: - _LOGGER.error("Something went wrong loading devices!") - return - - for device in devices["result"]: - self.handleDeviceDate(device) - - def handleDeviceDate(self, device): - self.devices[device["id"]] = {} - self.devices[device["id"]]["isThermostat"] = False - self.devices[device["id"]]["name"] = device["name"].strip() - self.devices[device["id"]]["online"] = device["online"] - self.devices[device["id"]]["update"] = device["update_time"] - if "model" in device: - self.devices[device["id"]]["model"] = device["model"] - elif "device_type" in device: - self.devices[device["id"]]["model"] = device["device_type"] - - bHasFloorSensor = False - for status in device["status"]: - if status["code"] == "floor_sensor": - bHasFloorSensor = status["value"] - self.devices[device["id"]]["floor_sensor"] = bHasFloorSensor - - for status in device["status"]: - # if status["code"] == "temp_set": - # setpoint = float(status["value"]) - # setpoint = setpoint / 10 - # self.devices[device["id"]]["setpoint"] = setpoint - # self.devices[device["id"]]["isThermostat"] = True - if status["code"] in ["manual_mode_fast", "at_home_setting", "leaving_home_setting", "pause_setting", "holiday_setting"]: - setpoint = float(status["value"]) - setpoint = setpoint / 10 - self.devices[device["id"]][status["code"]] = setpoint - self.devices[device["id"]]["isThermostat"] = True - elif status["code"] == "temp_current": - temperature = float(status["value"]) - temperature = temperature / 10 - self.devices[device["id"]]["temperature"] = temperature - elif status["code"] == "MeasuredValue" and bHasFloorSensor: # Floor sensor - temperature = float(status["value"]) - temperature = temperature / 10 - self.devices[device["id"]]["floor temperature"] = temperature - elif status["code"] == "upper_temp": - temperature = float(status["value"]) - temperature = temperature / 10 - self.devices[device["id"]]["upper_temp"] = temperature - elif status["code"] == "lower_temp": - temperature = float(status["value"]) - temperature = temperature / 10 - self.devices[device["id"]]["lower_temp"] = temperature - elif status["code"] == "va_temperature": - temperature = float(status["value"]) - temperature = temperature / 10 - self.devices[device["id"]]["temperature"] = temperature - elif status["code"] == "va_humidity": - humidity = float(status["value"]) - humidity = humidity / 10 - self.devices[device["id"]]["humidity"] = humidity - elif status["code"] == "battery_percentage": - battery = status["value"] - self.devices[device["id"]]["battery"] = battery - elif status["code"] == "window_state": - window = status["value"] - if window == "open": - self.devices[device["id"]]["window_open"] = True - else: - self.devices[device["id"]]["window_open"] = False - # elif status["code"] == "child_lock": - # childlock = status["value"] - # self.devices[device["id"]]["child_lock"] = childlock - # elif status["code"] == "mode": - # self.devices[device["id"]]["mode"] = status["value"] - # elif status["code"] == "work_state": - # self.devices[device["id"]]["work_state"] = status["value"] - if status["code"] in ["child_lock", "mode", "work_state", "banner_ctrl"]: - self.devices[device["id"]][status["code"]] = status["value"] - - def getDevice(self, device_id): - """Get device data.""" - device = self._api.get_device(device_id) - - if device is None or not device: - _LOGGER.error("No device loaded, API error?!") - return - if not "result" in device: - _LOGGER.error("Something went wrong loading devices!") - return - - self.handleDeviceDate(device["result"]) - - @property - def authorized(self): - """Return authorized status.""" - return self._authorized - - def setTemperature(self, device_id: str, temp: float, code = "manual_mode_fast") -> bool: - """Updates temperature setpoint for given device.""" - temperature = int(temp * 10) - - result = self._api.set_temperature(device_id, temperature, code) - - return result - - - def setMode(self, device_id: str, mode: str) -> bool: - """Updates operating mode for given device.""" - result = self._api.set_mode(device_id, mode) - - return result diff --git a/custom_components/danfoss_ally/pydanfossally/const.py b/custom_components/danfoss_ally/pydanfossally/const.py deleted file mode 100644 index 562a6eb..0000000 --- a/custom_components/danfoss_ally/pydanfossally/const.py +++ /dev/null @@ -1,3 +0,0 @@ -THERMOSTAT_MODE_AUTO = "hot" -THERMOSTAT_MODE_MANUAL = "manual" -THERMOSTAT_MODE_OFF = "pause" diff --git a/custom_components/danfoss_ally/pydanfossally/danfossallyapi.py b/custom_components/danfoss_ally/pydanfossally/danfossallyapi.py deleted file mode 100644 index a5cda86..0000000 --- a/custom_components/danfoss_ally/pydanfossally/danfossallyapi.py +++ /dev/null @@ -1,184 +0,0 @@ -import base64 -import datetime -import json -import logging - -import requests - -API_HOST = "https://api.danfoss.com" - -_LOGGER = logging.getLogger(__name__) - - -class DanfossAllyAPI: - def __init__(self): - """Init API.""" - self._key = "" - self._secret = "" - self._token = "" - self._refresh_at = datetime.datetime.now() - - def _call(self, path, headers_data, payload=None): - """Do the actual API call async.""" - - self._refresh_token() - try: - if payload: - req = requests.post( - API_HOST + path, json=payload, headers=headers_data, timeout=10 - ) - else: - req = requests.get(API_HOST + path, headers=headers_data, timeout=10) - - if not req.ok: - return False - except TimeoutError: - _LOGGER.warning("Timeout communication with Danfoss Ally API") - return False - except: - _LOGGER.warning( - "Unexpected error occured in communications with Danfoss Ally API!" - ) - return False - - return req.json() - - def _refresh_token(self): - """Refresh OAuth2 token if expired.""" - if self._refresh_at > datetime.datetime.now(): - return False - - self.getToken() - - def _generate_base64_token(self, key: str, secret: str) -> str: - """Generates a base64 token""" - key_secret = key + ":" + secret - key_secret_bytes = key_secret.encode("ascii") - base64_bytes = base64.b64encode(key_secret_bytes) - base64_token = base64_bytes.decode("ascii") - - return base64_token - - def getToken(self, key=None, secret=None) -> str: - """Get token.""" - - if not key is None: - self._key = key - if not secret is None: - self._secret = secret - - base64_token = self._generate_base64_token(self._key, self._secret) - - header_data = {} - header_data["Content-Type"] = "application/x-www-form-urlencoded" - header_data["Authorization"] = "Basic " + base64_token - header_data["Accept"] = "application/json" - - post_data = "grant_type=client_credentials" - try: - req = requests.post( - API_HOST + "/oauth2/token", - data=post_data, - headers=header_data, - timeout=10, - ) - - if not req.ok: - return False - except TimeoutError: - _LOGGER.warning("Timeout communication with Danfoss Ally API") - return False - except: - _LOGGER.warning( - "Unexpected error occured in communications with Danfoss Ally API!" - ) - return False - - callData = req.json() - - if callData is False: - return False - - expires_in = float(callData["expires_in"]) - self._refresh_at = datetime.datetime.now() - self._refresh_at = self._refresh_at + datetime.timedelta(seconds=expires_in) - self._refresh_at = self._refresh_at + datetime.timedelta(seconds=-30) - self._token = callData["access_token"] - return True - - def get_devices(self): - """Get list of all devices.""" - - header_data = {} - header_data["Accept"] = "application/json" - header_data["Authorization"] = "Bearer " + self._token - - callData = self._call("/ally/devices", header_data) - - return callData - - def get_device(self, device_id: str): - """Get device details.""" - - header_data = {} - header_data["Accept"] = "application/json" - header_data["Authorization"] = "Bearer " + self._token - - callData = self._call("/ally/devices/" + device_id, header_data) - - return callData - - def set_temperature(self, device_id: str, temp: int, code = "manual_mode_fast") -> bool: - """Set temperature setpoint.""" - - header_data = {} - header_data["Accept"] = "application/json" - header_data["Authorization"] = "Bearer " + self._token - - #request_body = {"commands": [{"code": "temp_set", "value": temp}]} - request_body = {"commands": [{"code": code, "value": temp}]} - - callData = self._call( - "/ally/devices/" + device_id + "/commands", header_data, request_body - ) - - _LOGGER.debug("Set temperature for device %s: %s", device_id, json.dumps(request_body)) - - return callData["result"] - - - - def set_mode(self, device_id: str, mode: str) -> bool: - """Set device operating mode.""" - - header_data = {} - header_data["Accept"] = "application/json" - header_data["Authorization"] = "Bearer " + self._token - - request_body = {"commands": [{"code": "mode", "value": mode}]} - - callData = self._call( - "/ally/devices/" + device_id + "/commands", header_data, request_body - ) - - return callData["result"] - - def set_mode(self, device_id, mode) -> bool: - """Set mode.""" - - header_data = {} - header_data["Accept"] = "application/json" - header_data["Authorization"] = "Bearer " + self._token - - request_body = {"commands": [{"code": "mode", "value": mode}]} - - callData = self._call( - "/ally/devices/" + device_id + "/commands", header_data, request_body - ) - - return callData["result"] - - @property - def token(self) -> str: - """Return token.""" - return self._token diff --git a/custom_components/danfoss_ally/sensor.py b/custom_components/danfoss_ally/sensor.py index 089c787..64cdf7a 100644 --- a/custom_components/danfoss_ally/sensor.py +++ b/custom_components/danfoss_ally/sensor.py @@ -1,22 +1,18 @@ """Support for Ally sensors.""" import logging +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, - DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ( - DATA, - DOMAIN, - SIGNAL_ALLY_UPDATE_RECEIVED, -) +from .const import DATA, DOMAIN, SIGNAL_ALLY_UPDATE_RECEIVED from .entity import AllyDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -33,7 +29,9 @@ async def async_setup_entry( for device in ally.devices: for sensor_type in ["battery", "temperature", "humidity", "floor temperature"]: if sensor_type in ally.devices[device]: - _LOGGER.debug("Found %s sensor for %s", sensor_type, ally.devices[device]["name"]) + _LOGGER.debug( + "Found %s sensor for %s", sensor_type, ally.devices[device]["name"] + ) entities.extend( [ AllySensor( From 5520750de409c01078ad5672f7f20a05ba7b373e Mon Sep 17 00:00:00 2001 From: Jan <20459196+jnxxx@users.noreply.github.com> Date: Mon, 19 Sep 2022 21:58:50 +0200 Subject: [PATCH 08/18] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 305c778..fc944b6 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,8 @@ Previous README ---
-[![](https://img.shields.io/github/release/mtrab/danfoss_ally/all.svg?style=plastic)](https://github.com/mtrab/danfoss_ally/releases) -[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=plastic)](https://github.com/custom-components/hacs) +[![danfoss_ally](https://img.shields.io/github/release/mtrab/danfoss_ally/all.svg?style=plastic&label=Current%20release)](https://github.com/mtrab/danfoss_ally) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=plastic)](https://github.com/custom-components/hacs) ![Validate with hassfest](https://img.shields.io/github/workflow/status/mtrab/danfoss_ally/Code%20validation?label=Hass%20validation&style=plastic) ![Maintenance](https://img.shields.io/maintenance/yes/2022.svg?style=plastic&label=Integration%20maintained) [![downloads](https://img.shields.io/github/downloads/mtrab/danfoss_ally/total?style=plastic&label=Total%20downloads)](https://github.com/mtrab/danfoss_ally) [![Buy me a coffee](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=and%20say%20thanks&color=orange&logo=buymeacoffee&logoColor=white&style=plastic)](https://www.buymeacoffee.com/mtrab) -Buy Me A Coffee # Danfoss Ally This is a custom component for Home Assistant to integrate the Danfoss Ally and Icon devices via Danfoss Ally gateway From 732b40629d9bffb3828448a9ba45c9bf6d982f05 Mon Sep 17 00:00:00 2001 From: Thomas Barnekov Date: Fri, 30 Sep 2022 11:19:20 +0200 Subject: [PATCH 09/18] Add specialization of AllyClimate for Icon RT to reflect correct working state --- custom_components/danfoss_ally/climate.py | 76 +++++++++++++++++++---- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/custom_components/danfoss_ally/climate.py b/custom_components/danfoss_ally/climate.py index e4fc358..3b90402 100644 --- a/custom_components/danfoss_ally/climate.py +++ b/custom_components/danfoss_ally/climate.py @@ -335,6 +335,47 @@ def get_setpoint_for_current_mode(self): return(setpoint) + +class IconClimate(AllyClimate): + """Representation of a Danfoss Icon climate entity.""" + + def __init__( + self, + ally, + name, + device_id, + model, + heat_min_temp, + heat_max_temp, + heat_step, + supported_hvac_modes, + support_flags, + ): + """Initialize Danfoss Icon climate entity.""" + super().__init__( + ally, + name, + device_id, + model, + heat_min_temp, + heat_max_temp, + heat_step, + supported_hvac_modes, + support_flags + ) + + @property + def hvac_action(self): + """Return the current running hvac operation if supported. + Need to be one of CURRENT_HVAC_*. + """ + if "work_state" in self._device: + if self._device["work_state"] == "heat_active": + return CURRENT_HVAC_HEAT + elif self._device["work_state"] == "Heat": + return CURRENT_HVAC_IDLE + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): @@ -379,15 +420,28 @@ def create_climate_entity(ally, name: str, device_id: str, model: str) -> AllyCl heat_max_temp = 35.0 heat_step = 0.5 - entity = AllyClimate( - ally, - name, - device_id, - model, - heat_min_temp, - heat_max_temp, - heat_step, - supported_hvac_modes, - support_flags, - ) + if model == 'Icon RT': + entity = IconClimate( + ally, + name, + device_id, + model, + heat_min_temp, + heat_max_temp, + heat_step, + supported_hvac_modes, + support_flags, + ) + else: + entity = AllyClimate( + ally, + name, + device_id, + model, + heat_min_temp, + heat_max_temp, + heat_step, + supported_hvac_modes, + support_flags, + ) return entity From f72f05ed0dea4e59f55180ea8b83c95361552a12 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Oct 2022 22:27:17 +0200 Subject: [PATCH 10/18] Remove domains from hacs.json --- hacs.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hacs.json b/hacs.json index 75dfcea..4c93f82 100644 --- a/hacs.json +++ b/hacs.json @@ -1,11 +1,6 @@ { "name": "Danfoss Ally", "render_readme": true, - "domains": [ - "binary_sensor", - "climate", - "sensor" - ], "homeassistant": "2022.1.0", "zip_release": true, "filename": "danfoss_ally.zip" From afa5abceb32a557121b8d96db48e0df90214d17b Mon Sep 17 00:00:00 2001 From: Thomas Barnekov Date: Sat, 22 Oct 2022 07:56:59 +0200 Subject: [PATCH 11/18] Fixed detection and setting of holiday mode --- custom_components/danfoss_ally/climate.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/custom_components/danfoss_ally/climate.py b/custom_components/danfoss_ally/climate.py index 3b90402..17fb6ae 100644 --- a/custom_components/danfoss_ally/climate.py +++ b/custom_components/danfoss_ally/climate.py @@ -26,7 +26,6 @@ from .const import ( DATA, DOMAIN, - HVAC_MODE_MANUAL, PRESET_MANUAL, PRESET_PAUSE, PRESET_HOLIDAY, @@ -146,7 +145,7 @@ def hvac_mode(self): or self._device["mode"] == "leaving_home" ): return HVAC_MODE_AUTO - elif (self._device["mode"] == "manual" or self._device["mode"] == "pause" or self._device["mode"] == "holiday"): + elif (self._device["mode"] == "manual" or self._device["mode"] == "pause" or self._device["mode"] == "holiday_sat"): return HVAC_MODE_HEAT @property @@ -161,7 +160,7 @@ def preset_mode(self): return PRESET_PAUSE elif self._device["mode"] == "manual": return PRESET_MANUAL - elif self._device["mode"] == "holiday": + elif self._device["mode"] == "holiday_sat": return PRESET_HOLIDAY @property @@ -190,7 +189,7 @@ def set_preset_mode(self, preset_mode): elif preset_mode == PRESET_MANUAL: mode = "manual" elif preset_mode == PRESET_HOLIDAY: - mode = "holiday" + mode = "holiday_sat" if mode is None: return @@ -321,7 +320,7 @@ def get_setpoint_code_for_mode(self, mode, for_writing = True): setpoint_code = "pause_setting" elif mode == "manual": setpoint_code = "manual_mode_fast" - elif mode == "holiday": + elif mode == "holiday_sat": setpoint_code = "holiday_setting" return setpoint_code From 00c86f54ee339ee2f17a203adf9fd129a14dd3ae Mon Sep 17 00:00:00 2001 From: Thomas Barnekov Date: Sat, 22 Oct 2022 15:44:40 +0200 Subject: [PATCH 12/18] Distinguish between holiday at home and holiday away --- custom_components/danfoss_ally/climate.py | 25 +++++++++++++++----- custom_components/danfoss_ally/const.py | 3 ++- custom_components/danfoss_ally/services.yaml | 4 +++- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/custom_components/danfoss_ally/climate.py b/custom_components/danfoss_ally/climate.py index 17fb6ae..7b9d86c 100644 --- a/custom_components/danfoss_ally/climate.py +++ b/custom_components/danfoss_ally/climate.py @@ -26,9 +26,10 @@ from .const import ( DATA, DOMAIN, + PRESET_HOLIDAY_AWAY, + PRESET_HOLIDAY_HOME, PRESET_MANUAL, PRESET_PAUSE, - PRESET_HOLIDAY, SIGNAL_ALLY_UPDATE_RECEIVED, ) from .entity import AllyDeviceEntity @@ -70,7 +71,8 @@ def __init__( PRESET_AWAY, PRESET_PAUSE, PRESET_MANUAL, - PRESET_HOLIDAY + PRESET_HOLIDAY_HOME, + PRESET_HOLIDAY_AWAY ] self._support_flags = support_flags @@ -143,9 +145,14 @@ def hvac_mode(self): if ( self._device["mode"] == "at_home" or self._device["mode"] == "leaving_home" + or self._device["mode"] == "holiday_sat" ): return HVAC_MODE_AUTO - elif (self._device["mode"] == "manual" or self._device["mode"] == "pause" or self._device["mode"] == "holiday_sat"): + elif ( + self._device["mode"] == "manual" + or self._device["mode"] == "pause" + or self._device["mode"] == "holiday" + ): return HVAC_MODE_HEAT @property @@ -161,7 +168,9 @@ def preset_mode(self): elif self._device["mode"] == "manual": return PRESET_MANUAL elif self._device["mode"] == "holiday_sat": - return PRESET_HOLIDAY + return PRESET_HOLIDAY_HOME + elif self._device["mode"] == "holiday": + return PRESET_HOLIDAY_AWAY @property def hvac_modes(self): @@ -188,8 +197,10 @@ def set_preset_mode(self, preset_mode): mode = "pause" elif preset_mode == PRESET_MANUAL: mode = "manual" - elif preset_mode == PRESET_HOLIDAY: + elif preset_mode == PRESET_HOLIDAY_HOME: mode = "holiday_sat" + elif preset_mode == PRESET_HOLIDAY_AWAY: + mode = "holiday" if mode is None: return @@ -320,8 +331,10 @@ def get_setpoint_code_for_mode(self, mode, for_writing = True): setpoint_code = "pause_setting" elif mode == "manual": setpoint_code = "manual_mode_fast" - elif mode == "holiday_sat": + elif mode == "holiday": setpoint_code = "holiday_setting" + elif mode == "holiday_sat": + setpoint_code = "at_home_setting" return setpoint_code def get_setpoint_for_current_mode(self): diff --git a/custom_components/danfoss_ally/const.py b/custom_components/danfoss_ally/const.py index 3679417..46a11b1 100644 --- a/custom_components/danfoss_ally/const.py +++ b/custom_components/danfoss_ally/const.py @@ -13,7 +13,8 @@ PRESET_MANUAL = "Manual" PRESET_PAUSE = "Pause" -PRESET_HOLIDAY = "Holiday" +PRESET_HOLIDAY_AWAY = "Holiday (Away)" +PRESET_HOLIDAY_HOME = "Holiday (Home)" HA_TO_DANFOSS_HVAC_MODE_MAP = { HVAC_MODE_OFF: THERMOSTAT_MODE_OFF, diff --git a/custom_components/danfoss_ally/services.yaml b/custom_components/danfoss_ally/services.yaml index 6d54e63..5947810 100644 --- a/custom_components/danfoss_ally/services.yaml +++ b/custom_components/danfoss_ally/services.yaml @@ -30,5 +30,7 @@ set_preset_temperature: value: "manual" - label: "Pause" value: "pause" - - label: "Holiday" + - label: "Holiday (Home)" + value: "holiday_sat" + - label: "Holiday (Away)" value: "holiday" From f315b63a4e5ae41372f478f0784696d4c73dc0e3 Mon Sep 17 00:00:00 2001 From: Thomas Barnekov Date: Sat, 22 Oct 2022 16:19:15 +0200 Subject: [PATCH 13/18] General cleaning --- custom_components/danfoss_ally/__init__.py | 4 +-- .../danfoss_ally/binary_sensor.py | 5 +--- custom_components/danfoss_ally/climate.py | 26 +++++++------------ custom_components/danfoss_ally/config_flow.py | 1 - custom_components/danfoss_ally/const.py | 3 --- custom_components/danfoss_ally/entity.py | 25 ------------------ custom_components/danfoss_ally/strings.json | 4 +-- 7 files changed, 14 insertions(+), 54 deletions(-) diff --git a/custom_components/danfoss_ally/__init__.py b/custom_components/danfoss_ally/__init__.py index 3efa539..7b8ad35 100644 --- a/custom_components/danfoss_ally/__init__.py +++ b/custom_components/danfoss_ally/__init__.py @@ -92,11 +92,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def _update(now): """Periodic update.""" await allyconnector.async_update() - await _update(None) #await hass.async_add_executor_job(allyconnector.update) + await _update(None) update_track = async_track_time_interval( hass, - _update, #lambda now: allyconnector.update(), + _update, timedelta(seconds=SCAN_INTERVAL), ) diff --git a/custom_components/danfoss_ally/binary_sensor.py b/custom_components/danfoss_ally/binary_sensor.py index 3ef9245..123d32c 100644 --- a/custom_components/danfoss_ally/binary_sensor.py +++ b/custom_components/danfoss_ally/binary_sensor.py @@ -4,8 +4,8 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_LOCK, - DEVICE_CLASS_WINDOW, DEVICE_CLASS_TAMPER, + DEVICE_CLASS_WINDOW, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -69,8 +69,6 @@ async def async_setup_entry( ) ] ) - - if entities: async_add_entities(entities, True) @@ -106,7 +104,6 @@ def __init__(self, ally, name, device_id, device_type, model): elif self._type == "banner control": self._state = bool(self._device["banner_ctrl"]) - async def async_added_to_hass(self): """Register for sensor updates.""" diff --git a/custom_components/danfoss_ally/climate.py b/custom_components/danfoss_ally/climate.py index 7b9d86c..a22b66c 100644 --- a/custom_components/danfoss_ally/climate.py +++ b/custom_components/danfoss_ally/climate.py @@ -4,6 +4,8 @@ import voluptuous as vol from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, @@ -11,15 +13,13 @@ PRESET_AWAY, PRESET_HOME, SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, - ATTR_PRESET_MODE, - ATTR_HVAC_MODE, + SUPPORT_TARGET_TEMPERATURE ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers import entity_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect import functools as ft from . import AllyConnector @@ -83,7 +83,7 @@ def __init__( self._cur_temp = self._device["temperature"] else: # TEMPORARY fix for missing temperature sensor - self._cur_temp = self.get_setpoint_for_current_mode() #self._device["setpoint"] + self._cur_temp = self.get_setpoint_for_current_mode() # Low temperature set in Ally app if "lower_temp" in self._device: @@ -133,8 +133,7 @@ def current_temperature(self): if "temperature" in self._device: return self._device["temperature"] else: - # TEMPORARY fix for missing temperature sensor - return self.get_setpoint_for_current_mode() #self._device["setpoint"] + return self.get_setpoint_for_current_mode() @property def hvac_mode(self): @@ -235,11 +234,10 @@ def target_temperature_step(self): @property def target_temperature(self): """Return the temperature we try to reach.""" - return self.get_setpoint_for_current_mode() #self._device["setpoint"] + return self.get_setpoint_for_current_mode() def set_temperature(self, **kwargs): """Set new target temperature.""" - if ATTR_TEMPERATURE in kwargs: temperature = kwargs.get(ATTR_TEMPERATURE) @@ -253,7 +251,6 @@ def set_temperature(self, **kwargs): setpoint_code = self.get_setpoint_code_for_mode("manual") else: setpoint_code = self.get_setpoint_code_for_mode(self._device["mode"]) # Current preset_mode - #_LOGGER.debug("setpoint_code: %s", setpoint_code) changed = False if temperature is not None and setpoint_code is not None: @@ -270,7 +267,6 @@ async def set_preset_temperature(self, **kwargs): ft.partial(self.set_temperature, **kwargs) ) - @property def available(self): """Return if the device is available.""" @@ -317,7 +313,6 @@ def set_hvac_mode(self, hvac_mode): # Update UI self.async_write_ha_state() - def get_setpoint_code_for_mode(self, mode, for_writing = True): setpoint_code = None if for_writing == False and "banner_ctrl" in self._device and bool(self._device['banner_ctrl']): @@ -345,7 +340,7 @@ def get_setpoint_for_current_mode(self): if setpoint_code is not None and setpoint_code in self._device: setpoint = self._device[setpoint_code] - return(setpoint) + return setpoint class IconClimate(AllyClimate): @@ -361,7 +356,7 @@ def __init__( heat_max_temp, heat_step, supported_hvac_modes, - support_flags, + support_flags ): """Initialize Danfoss Icon climate entity.""" super().__init__( @@ -405,7 +400,6 @@ async def async_setup_entry( ally: AllyConnector = hass.data[DOMAIN][entry.entry_id][DATA] entities = await hass.async_add_executor_job(_generate_entities, ally) - #_LOGGER.debug(ally.devices) if entities: async_add_entities(entities, True) @@ -432,7 +426,7 @@ def create_climate_entity(ally, name: str, device_id: str, model: str) -> AllyCl heat_max_temp = 35.0 heat_step = 0.5 - if model == 'Icon RT': + if model == "Icon RT": entity = IconClimate( ally, name, diff --git a/custom_components/danfoss_ally/config_flow.py b/custom_components/danfoss_ally/config_flow.py index 1bd9218..8caab2d 100644 --- a/custom_components/danfoss_ally/config_flow.py +++ b/custom_components/danfoss_ally/config_flow.py @@ -3,7 +3,6 @@ import requests.exceptions import voluptuous as vol - from homeassistant import config_entries, core, exceptions from homeassistant.core import callback from pydanfossally import DanfossAlly diff --git a/custom_components/danfoss_ally/const.py b/custom_components/danfoss_ally/const.py index 46a11b1..803bcda 100644 --- a/custom_components/danfoss_ally/const.py +++ b/custom_components/danfoss_ally/const.py @@ -38,6 +38,3 @@ ACTION_TYPE_SET_PRESET_TEMPERATURE = "set_preset_temperature" ATTR_SETPOINT = "setpoint" - - - diff --git a/custom_components/danfoss_ally/entity.py b/custom_components/danfoss_ally/entity.py index 639b7c9..c3c18da 100644 --- a/custom_components/danfoss_ally/entity.py +++ b/custom_components/danfoss_ally/entity.py @@ -29,28 +29,3 @@ def device_info(self): def should_poll(self): """Do not poll.""" return False - - -class AllyClimateEntity(Entity): - """Base implementation for Danfoss Ally Thermostat.""" - - def __init__(self, name, device_id): - """Initialize a Danfoss Ally zone.""" - super().__init__() - self._device_id = device_id - self._name = name - - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._device_id)}, - "name": self._name, - "manufacturer": DEFAULT_NAME, - "model": None, - } - - @property - def should_poll(self): - """Do not poll.""" - return False diff --git a/custom_components/danfoss_ally/strings.json b/custom_components/danfoss_ally/strings.json index ef7485c..5b664ed 100644 --- a/custom_components/danfoss_ally/strings.json +++ b/custom_components/danfoss_ally/strings.json @@ -20,7 +20,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, - "device_automation": { "condition_type": { }, @@ -30,5 +29,4 @@ "set_preset_temperature": "Set preset temperature" } } - -} + } \ No newline at end of file From b2b8a4863c6b279c7fb19936f09b7c69429bbd0e Mon Sep 17 00:00:00 2001 From: Thomas Barnekov Date: Sat, 22 Oct 2022 16:32:17 +0200 Subject: [PATCH 14/18] Fix merge error --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index caa08b1..cabb1b5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -<<<<<<< HEAD -======= # Danfoss Ally @@ -48,7 +46,6 @@ Previous README ---
->>>>>>> master [![danfoss_ally](https://img.shields.io/github/release/mtrab/danfoss_ally/all.svg?style=plastic&label=Current%20release)](https://github.com/mtrab/danfoss_ally) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=plastic)](https://github.com/custom-components/hacs) ![Validate with hassfest](https://img.shields.io/github/workflow/status/mtrab/danfoss_ally/Code%20validation?label=Hass%20validation&style=plastic) ![Maintenance](https://img.shields.io/maintenance/yes/2022.svg?style=plastic&label=Integration%20maintained) [![downloads](https://img.shields.io/github/downloads/mtrab/danfoss_ally/total?style=plastic&label=Total%20downloads)](https://github.com/mtrab/danfoss_ally) [![Buy me a coffee](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=and%20say%20thanks&color=orange&logo=buymeacoffee&logoColor=white&style=plastic)](https://www.buymeacoffee.com/mtrab) # Danfoss Ally From 3116adcbcd2028c11e45d2e20d61325564a8ba76 Mon Sep 17 00:00:00 2001 From: Thomas Barnekov Date: Sat, 22 Oct 2022 21:46:11 +0200 Subject: [PATCH 15/18] Fix broken sensors --- custom_components/danfoss_ally/sensor.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/custom_components/danfoss_ally/sensor.py b/custom_components/danfoss_ally/sensor.py index 32e68d4..504e546 100644 --- a/custom_components/danfoss_ally/sensor.py +++ b/custom_components/danfoss_ally/sensor.py @@ -30,7 +30,6 @@ class AllySensorType(IntEnum): BATTERY = 1 HUMIDITY = 2 - SENSORS = [ SensorEntityDescription( key=AllySensorType.TEMPERATURE, @@ -55,7 +54,7 @@ class AllySensorType(IntEnum): native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, name="{} Humidity", - ), + ) ] @@ -69,15 +68,16 @@ async def async_setup_entry( for device in ally.devices: for sensor in SENSORS: - sensor_type = AllySensorType(sensor.key).name + sensor_type = AllySensorType(sensor.key).name.lower() if sensor_type in ally.devices[device]: _LOGGER.debug( "Found %s sensor for %s", sensor_type, ally.devices[device]["name"] ) entities.extend( - [AllySensor(ally, ally.devices[device]["name"], device, sensor)] + [AllySensor(ally, ally.devices[device]["name"], device, sensor, ally.devices[device]["model"])] ) + if entities: async_add_entities(entities, True) @@ -86,18 +86,19 @@ class AllySensor(AllyDeviceEntity, SensorEntity): """Representation of an Ally sensor.""" def __init__( - self, ally: DanfossAlly, name, device_id, description: SensorEntityDescription + self, ally: DanfossAlly, name, device_id, description: SensorEntityDescription, model = None ): """Initialize Ally binary_sensor.""" self.entity_description = description self._ally = ally self._device = ally.devices[device_id] self._device_id = device_id - self._type = AllySensorType(description.key).name - super().__init__(name, device_id, self._type) + self._type = AllySensorType(description.key).name.lower() + super().__init__(name, device_id, self._type, model) _LOGGER.debug("Device_id: %s --- Device: %s", self._device_id, self._device) - + _LOGGER.debug("Creating Ally sensor %s --- Device: %s --- Model: %s --- Type: %s", self._device_id, self._device, model, self._type) + self._attr_native_value = None self._attr_extra_state_attributes = None self._attr_name = self.entity_description.name.format(name) @@ -123,7 +124,7 @@ def _async_update_callback(self): @callback def _async_update_data(self): """Load data.""" - _LOGGER.debug("Loading new sensor data for device %s", self._device_id) + _LOGGER.debug("Loading new sensor data for Ally Sensor for device %s", self._device_id) self._device = self._ally.devices[self._device_id] if self._type == "battery": From b1b202e0daaf49da6987f7d0ebcb542bfa5aa473 Mon Sep 17 00:00:00 2001 From: Thomas Barnekov Date: Sat, 22 Oct 2022 21:49:09 +0200 Subject: [PATCH 16/18] Simplify sensor data update --- custom_components/danfoss_ally/sensor.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/custom_components/danfoss_ally/sensor.py b/custom_components/danfoss_ally/sensor.py index 504e546..ac09536 100644 --- a/custom_components/danfoss_ally/sensor.py +++ b/custom_components/danfoss_ally/sensor.py @@ -97,7 +97,6 @@ def __init__( super().__init__(name, device_id, self._type, model) _LOGGER.debug("Device_id: %s --- Device: %s", self._device_id, self._device) - _LOGGER.debug("Creating Ally sensor %s --- Device: %s --- Model: %s --- Type: %s", self._device_id, self._device, model, self._type) self._attr_native_value = None self._attr_extra_state_attributes = None @@ -127,9 +126,5 @@ def _async_update_data(self): _LOGGER.debug("Loading new sensor data for Ally Sensor for device %s", self._device_id) self._device = self._ally.devices[self._device_id] - if self._type == "battery": - self._attr_native_value = self._device["battery"] - elif self._type == "temperature": - self._attr_native_value = self._device["temperature"] - elif self._type == "humidity": - self._attr_native_value = self._device["humidity"] + if self._type in self._device: + self._attr_native_value = self._device[self._type] From 97d4ad3ddbf7563bb330635d2da522d0b9081d9a Mon Sep 17 00:00:00 2001 From: Thomas Barnekov Date: Sat, 22 Oct 2022 21:56:27 +0200 Subject: [PATCH 17/18] Revert README.md --- README.md | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/README.md b/README.md index cabb1b5..970d6b2 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,3 @@ -# Danfoss Ally - - -This integration is forked from: **[MTrab / danfoss_ally](https://github.com/MTrab/danfoss_ally)** v1.0.7 - -### Additions - -- Reading and writing setpoints using: `manual_mode_fast`, `at_home_setting`, `leaving_home_setting`, `pause_setting`, `holiday_setting` depending on the preset mode, rather than using `temp_set` as before. -It seems to work, so far. - -- Holiday preset mode added. - -- Quicker reaction to changes performed in the UI - -- Added floor temperature sensor - -- Fix for setmode issue - -- Added action and service call to set target temperature for a specific preset mode. -Preset mode is optional, and writes to current preset mode when not specified. - -- Added an indication for 'banner control' (local override). -When setpoint is changed locally from the thermostate it raises this flag and uses this as manual target setpoint. - - -##### Things to note in the Danfoss Ally app - -- The app shows the floor temperature (when present) in the overview, and the room temperature on the details page. That is somewhat confusing, I think. Especially when it doesn't indicate it which is which. - -- When switching to manual mode from the app, it will take the previous taget temparature as target also for manual. Thus, overwrite target temperature for manual preset. -Switching from this integration will just switch to manual and not overwrite target temperature, unless specifically set. - -*Note: Changes are limited tested with radiator thermostates* -
-
-
-
-
-
-
-
- ---- -Previous README - ---- -
- [![danfoss_ally](https://img.shields.io/github/release/mtrab/danfoss_ally/all.svg?style=plastic&label=Current%20release)](https://github.com/mtrab/danfoss_ally) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=plastic)](https://github.com/custom-components/hacs) ![Validate with hassfest](https://img.shields.io/github/workflow/status/mtrab/danfoss_ally/Code%20validation?label=Hass%20validation&style=plastic) ![Maintenance](https://img.shields.io/maintenance/yes/2022.svg?style=plastic&label=Integration%20maintained) [![downloads](https://img.shields.io/github/downloads/mtrab/danfoss_ally/total?style=plastic&label=Total%20downloads)](https://github.com/mtrab/danfoss_ally) [![Buy me a coffee](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=and%20say%20thanks&color=orange&logo=buymeacoffee&logoColor=white&style=plastic)](https://www.buymeacoffee.com/mtrab) # Danfoss Ally From 5c603e4bfa55f6077aa7801dec439c25c16c456d Mon Sep 17 00:00:00 2001 From: Thomas Barnekov Date: Sat, 22 Oct 2022 22:17:43 +0200 Subject: [PATCH 18/18] Change type of sensor to lower case in sensor name --- custom_components/danfoss_ally/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/danfoss_ally/sensor.py b/custom_components/danfoss_ally/sensor.py index ac09536..1099335 100644 --- a/custom_components/danfoss_ally/sensor.py +++ b/custom_components/danfoss_ally/sensor.py @@ -37,7 +37,7 @@ class AllySensorType(IntEnum): entity_category=None, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, - name="{} Temperature", + name="{} temperature", ), SensorEntityDescription( key=AllySensorType.BATTERY, @@ -45,7 +45,7 @@ class AllySensorType(IntEnum): entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, - name="{} Battery", + name="{} battery", ), SensorEntityDescription( key=AllySensorType.HUMIDITY, @@ -53,7 +53,7 @@ class AllySensorType(IntEnum): entity_category=None, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, - name="{} Humidity", + name="{} humidity", ) ]