From 24fad3fafc17227a166dcdf6a28afec396e7400f Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 22 Apr 2026 21:55:10 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Implement=20zero=E2=80=91config=20detection?= =?UTF-8?q?,=20AC=20integration,=20persistent=20schedule=20via=20API=20cal?= =?UTF-8?q?l,=20HA=20notification=20and=20secure=20diagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/tado_local.zip | Bin 0 -> 22481 bytes custom_components/tado_local/__init__.py | 23 +++++ custom_components/tado_local/binary_sensor.py | 47 ++++++++- custom_components/tado_local/climate.py | 96 +++++++++++++++--- custom_components/tado_local/config_flow.py | 79 +++++++++++++- custom_components/tado_local/diagnostics.py | 32 +++++- custom_components/tado_local/manifest.json | 10 +- custom_components/tado_local/services.py | 32 +++--- custom_components/tado_local/services.yaml | 16 ++- custom_components/tado_local/strings.json | 40 ++++++-- .../tado_local/translations/de.json | 72 ++++++++++++- .../tado_local/translations/en.json | 40 ++++++-- .../tado_local/translations/it.json | 32 +++++- .../tado_local/translations/nl.json | 38 ++++++- 14 files changed, 490 insertions(+), 67 deletions(-) create mode 100644 custom_components/tado_local.zip diff --git a/custom_components/tado_local.zip b/custom_components/tado_local.zip new file mode 100644 index 0000000000000000000000000000000000000000..83a36ee36b6f8014d1d5705ab46026237d64e27d GIT binary patch literal 22481 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Ð+K1+ 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 HVACMode: mode = self._zone_data.get("mode") + # It takes a while for Tado to update the mode after a preset change, + # so we need wait a few updates before adjusting the preset mode + if self._update_preset_count > 0: + self._update_preset_count -= 1 + if mode == 0: + if self._attr_preset_mode == PRESET_HOME and self._update_preset_count == 0: + self._attr_preset_mode = PRESET_NONE return HVACMode.OFF - return HVACMode.HEAT + + if self._attr_preset_mode == PRESET_HOME: + return HVACMode.AUTO + + if self._attr_preset_mode == PRESET_AWAY and self._update_preset_count == 0: + self._attr_preset_mode = PRESET_NONE + return HVACMode.COOL if (mode == 2 and self._can_cool) else HVACMode.HEAT async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + if hvac_mode == HVACMode.AUTO: + self._attr_preset_mode = PRESET_NONE + await self._async_send_zone_update(temperature=-1) # Tado will decide the mode based on previous mode + return + + mode = None temp_param = None if hvac_mode == HVACMode.OFF: + mode = 0 # TadoLocal will igrore the temperature 0 param when heating mode is supported in newer API temp_param = 0 - elif hvac_mode == HVACMode.AUTO: - temp_param = -1 elif hvac_mode == HVACMode.HEAT: - target = self.target_temperature - if target is None or target < 5: - target = 21 - temp_param = target + mode = 1 + temp_param = -1 # TadoLocal will igrore the temperature -1 param when heating mode is supported in newer API + elif hvac_mode == HVACMode.COOL and self._can_cool: + mode = 2 + else: + _LOGGER.debug("Errore invalid mode") + return + + # For backwards API compatibility we need to send temperature param for mode change, will be ignored by Tado if heating_mode is supported + await self._async_send_zone_update(temperature=temp_param, mode=mode) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + self._attr_preset_mode = preset_mode + + if preset_mode == PRESET_NONE: + return + + # It takes a while for Tado to update the mode after a preset change, + # so we need to track the last preset and reset it after a few updates + # This is a workaround to avoid sending a preset mode again pending the previous one + # wasting API calls + if self._update_preset_count > 0: + return + self._update_preset_count = 4 + + url = f"{self._base_url}/zones/{self._zone_id}/set" + params = { + "persistant": "true", + "heating_enabled": "true" if preset_mode == PRESET_HOME else "false" + } - if temp_param is not None: - await self._async_send_zone_update(temp_param) + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, params=params) as response: + if response.status != 200: + _LOGGER.error("Errore update Tado: %s", await response.text()) + else: + await self.coordinator.async_request_refresh() + except Exception as err: + _LOGGER.error("Errore connessione update: %s", err) + + _LOGGER.debug("Preset: %s ", self._attr_preset_mode) + self._attr_preset_mode = preset_mode async def async_set_temperature(self, **kwargs) -> None: temp = kwargs.get(ATTR_TEMPERATURE) @@ -115,9 +181,15 @@ async def async_set_temperature(self, **kwargs) -> None: return await self._async_send_zone_update(temp) - async def _async_send_zone_update(self, temperature): + async def _async_send_zone_update(self, temperature=None, mode=None): url = f"{self._base_url}/zones/{self._zone_id}/set" - params = {"temperature": str(temperature)} + params = {} + if temperature is not None: + params["temperature"] = str(temperature) + if mode is not None: + params["heating_mode"] = str(mode) + if len(params) == 0: + return async with aiohttp.ClientSession() as session: try: diff --git a/custom_components/tado_local/config_flow.py b/custom_components/tado_local/config_flow.py index a7e9f11..6b67ffe 100644 --- a/custom_components/tado_local/config_flow.py +++ b/custom_components/tado_local/config_flow.py @@ -29,6 +29,10 @@ async def validate_input(hass: HomeAssistant, data: Dict[str, Any]) -> None: async with session.get(url) as response: if response.status != 200: raise Exception("Status code not 200") + js = await response.json() + if js.get("service") != "Tado Local": + raise Exception("not_tado_local") + class TadoLocalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Gestisce il flusso di configurazione per Tado Local.""" @@ -42,6 +46,9 @@ def async_get_options_flow(config_entry): # Passiamo la config_entry al costruttore return TadoLocalOptionsFlowHandler(config_entry) + # ------------------------------------------------------------------------- + # USER STEP (manual setup) + # ------------------------------------------------------------------------- async def async_step_user( self, user_input: Optional[Dict[str, Any]] = None ) -> FlowResult: @@ -54,8 +61,11 @@ async def async_step_user( except Exception: errors["base"] = "cannot_connect" else: + await self.async_set_unique_id("tado_local_server") + self._abort_if_unique_id_configured() + return self.async_create_entry( - title=f"Tado Local ({user_input[CONF_IP_ADDRESS]})", + title=f"Tado Local ({user_input[CONF_IP_ADDRESS]})", data=user_input ) @@ -69,6 +79,73 @@ async def async_step_user( step_id="user", data_schema=data_schema, errors=errors ) + # ------------------------------------------------------------------------- + # ZEROCONF DISCOVERY STEP + # ------------------------------------------------------------------------- + async def async_step_zeroconf(self, discovery_info) -> FlowResult: + """Handle mDNS discovery of tado-local.""" + host = discovery_info.host + port = discovery_info.port or 4407 + + # Validate the discovered server + try: + url = f"http://{host}:{port}/api" + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=5) as resp: + if resp.status != 200: + return self.async_abort(reason="cannot_connect") + data = await resp.json() + if data.get("service") != "Tado Local": + return self.async_abort(reason="not_tado_local") + except Exception: + return self.async_abort(reason="cannot_connect") + + # Prevent duplicates + await self.async_set_unique_id("tado_local_server") + self._abort_if_unique_id_configured() + + # Store discovery info for confirm step + self._discovered_host = host + self._discovered_port = port + + return await self.async_step_confirm() + + # ------------------------------------------------------------------------- + # CONFIRMATION STEP + # ------------------------------------------------------------------------- + async def async_step_confirm(self, user_input=None) -> FlowResult: + def _is_local_address(ip: str) -> bool: + import socket + try: + host_ips = socket.gethostbyname_ex(socket.gethostname())[2] + return ip in host_ips + except Exception: + return False + + display_ip = "localhost" if _is_local_address(self._discovered_host) else self._discovered_host + + """Ask user to confirm adding the discovered server.""" + if user_input is not None: + return self.async_create_entry( + title=f"Tado Local ({display_ip})", + data={ + CONF_IP_ADDRESS: self._discovered_host, # keep real IP internally + CONF_PORT: self._discovered_port, + CONF_UPDATE_INTERVAL: DEFAULT_UPDATE_INTERVAL, + } + ) + + return self.async_show_form( + step_id="confirm", + description_placeholders={ + "ip": display_ip, + "port": self._discovered_port, + } + ) + +# ----------------------------------------------------------------------------- +# OPTIONS FLOW +# ----------------------------------------------------------------------------- class TadoLocalOptionsFlowHandler(config_entries.OptionsFlow): """Gestisce la riconfigurazione delle opzioni.""" diff --git a/custom_components/tado_local/diagnostics.py b/custom_components/tado_local/diagnostics.py index fd559ad..8e1a0b9 100644 --- a/custom_components/tado_local/diagnostics.py +++ b/custom_components/tado_local/diagnostics.py @@ -15,5 +15,35 @@ async def async_get_config_entry_diagnostics(hass: HomeAssistant, entry: ConfigE coordinator = data["coordinator"] return { - "data": coordinator.data, + "data": _hide_sensitive_info(coordinator.data), } + +def _hide_sensitive_info(data: Any) -> Any: + """Recursively sanitize sensitive values in diagnostics data.""" + if isinstance(data, dict): + return { + key: _mask_serial_number(value) + if key in ["leader_serial", "serial_number"] + else "**secret**" + if key in ["home_id", "uuid"] + else _hide_sensitive_info(value) + for key, value in data.items() + } + + if isinstance(data, list): + return [_hide_sensitive_info(item) for item in data] + + return data + +def _mask_serial_number(value: Any) -> Any: + """Mask characters at positions 3 through 9 with 'x'.""" + if not isinstance(value, str): + return value + + if len(value) < 3: + return value + + start_index = 2 + end_index = min(9, len(value)) + + return f"{value[:start_index]}{'x' * (end_index - start_index)}{value[end_index:]}" diff --git a/custom_components/tado_local/manifest.json b/custom_components/tado_local/manifest.json index 3631f09..7361cae 100644 --- a/custom_components/tado_local/manifest.json +++ b/custom_components/tado_local/manifest.json @@ -4,7 +4,15 @@ "codeowners": [], "config_flow": true, "documentation": "https://github.com/ampscm/TadoLocal", + "issue_tracker": "https://github.com/ampscm/TadoLocal/issues", "requirements": [], "iot_class": "local_push", - "version": "1.0.3" + "version": "1.1.2", + "loggers": ["custom_components.tado_local"], + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "tado-local*" + } + ] } \ No newline at end of file diff --git a/custom_components/tado_local/services.py b/custom_components/tado_local/services.py index 35c650b..eaa4cf1 100644 --- a/custom_components/tado_local/services.py +++ b/custom_components/tado_local/services.py @@ -4,12 +4,11 @@ import logging import aiohttp -from typing import Any, Dict from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, CONF_IP_ADDRESS, CONF_PORT, CONF_UPDATE_INTERVAL, PLATFORMS +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -18,9 +17,12 @@ async def async_setup_services( ) -> None: """Set up the services for Tado Local.""" - async def _async_send_zone_update(zone_id, temperature): - url = f"{base_url}/zones/{zone_id}/set" - params = {"temperature": str(temperature)} + async def _async_send_all_zones_update(heating_enabled: bool, persistent: bool): + url = f"{base_url}/zones/set" + params = { + "heating_enabled": "true" if heating_enabled else "false", + "persistant": "true" if persistent else "false", + } async with aiohttp.ClientSession() as session: try: @@ -33,24 +35,16 @@ async def _async_send_zone_update(zone_id, temperature): async def handle_resume_schedules(call: ServiceCall) -> None: """Service to resume all schedules.""" - _LOGGER.debug("Service call: resume_all_schedules") - current_data = coordinator.data - zones_list = current_data.get("zones", []) - for zone in zones_list: - zid = zone.get("zone_id") or zone.get("id") - await _async_send_zone_update(zid, -1) - + persistent = call.data.get("persistent", False) + _LOGGER.debug("Service call: resume_all_schedules(persistent=%s)", persistent) + await _async_send_all_zones_update(True, persistent) await coordinator.async_request_refresh() async def handle_turn_off_all(call: ServiceCall) -> None: """Service to turn off all zones.""" - _LOGGER.debug("Service call: turn_off_all_zones") - current_data = coordinator.data - zones_list = current_data.get("zones", []) - for zone in zones_list: - zid = zone.get("zone_id") or zone.get("id") - await _async_send_zone_update(zid, 0) - + persistent = call.data.get("persistent", False) + _LOGGER.debug("Service call: turn_off_all_zones(persistent=%s)", persistent) + await _async_send_all_zones_update(False, persistent) await coordinator.async_request_refresh() hass.services.async_register( diff --git a/custom_components/tado_local/services.yaml b/custom_components/tado_local/services.yaml index db22aef..b0385d0 100644 --- a/custom_components/tado_local/services.yaml +++ b/custom_components/tado_local/services.yaml @@ -1,5 +1,19 @@ turn_off_all_zones: description: "Turn off heating for all zones." + fields: + persistent: + required: true + description: "Whether to make the change persistent (=true, send to Cloud) or temporary (=false) until the next schedule change." + example: "false" + selector: + boolean: resume_all_schedules: - description: "Resume schedule for all zones." \ No newline at end of file + description: "Resume schedule for all zones." + fields: + persistent: + required: true + description: "Whether to make the change persistent (=true, send to Cloud) or temporary (=false) until the next schedule change." + example: "false" + selector: + boolean: diff --git a/custom_components/tado_local/strings.json b/custom_components/tado_local/strings.json index 2383068..16785a9 100644 --- a/custom_components/tado_local/strings.json +++ b/custom_components/tado_local/strings.json @@ -3,20 +3,27 @@ "step": { "user": { "title": "Tado Local", - "description": "Connect to Tado Local API.", + "description": "Connect to the Tado Local API.", "data": { "ip_address": "IP Address", "port": "Port", "update_interval": "Update Interval (seconds)" } + }, + "confirm": { + "title": "Tado Local Discovered", + "description": "A Tado Local server was found at {ip}:{port}. Do you want to set it up?" } }, "error": { "cannot_connect": "Failed to connect.", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "not_tado_local": "The discovered device is not a Tado Local server" }, "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect to the discovered server", + "not_tado_local": "The discovered device is not a Tado Local server" } }, "options": { @@ -72,7 +79,10 @@ "name": "Cloud API authentication" }, "heating_active": { - "name": "Status" + "name": "Heating" + }, + "cooling_active": { + "name": "Cooling" }, "battery_low": { "name": "Battery" @@ -89,12 +99,24 @@ }, "services": { "resume_all_schedules": { - "description": "Instruct Tado to resume to the smart schedule for all zones.", - "name": "Resume schedule for all zones" + "name": "Resume Schedule for All Zones", + "description": "Instruct Tado to resume the smart schedule for all zones.", + "fields": { + "persistent": { + "name": "Persistent (or temporary)", + "description": "Whether to make the change persistent (=true, send to Cloud) or temporary (=false) until the next schedule change." + } + } }, "turn_off_all_zones": { - "description": "Instruct Tado to stop the smart schedule for all zones, add go to frost protection.", - "name": "Turn off heating for all zones" + "name": "Turn Off Heating for All Zones", + "description": "Instruct Tado to stop the smart schedule for all zones and switch to frost protection.", + "fields": { + "persistent": { + "name": "Persistent (or temporary)", + "description": "Whether to make the change persistent (=true, send to Cloud) or temporary (=false) until the next schedule change." + } + } } } -} \ No newline at end of file +} diff --git a/custom_components/tado_local/translations/de.json b/custom_components/tado_local/translations/de.json index 7c90f16..80de93f 100644 --- a/custom_components/tado_local/translations/de.json +++ b/custom_components/tado_local/translations/de.json @@ -9,14 +9,21 @@ "port": "Port", "update_interval": "Update Intervall (Sekunden)" } + }, + "confirm": { + "title": "Tado Local entdeckt", + "description": "Ein Tado Local Server wurde unter {ip}:{port} gefunden. Möchtest du ihn einrichten?" } }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "not_tado_local": "Kein 'Tado Local' Dienst unter der angegebenen IP Adresse und Port gefunden" }, "abort": { - "already_configured": "Gerät ist bereits konfiguriert" + "already_configured": "Gerät ist bereits konfiguriert", + "cannot_connect": "Verbindung zum gefundenen Server fehlgeschlagen", + "not_tado_local": "Das gefundene Gerät ist kein Tado Local Server" } }, "options": { @@ -44,15 +51,72 @@ }, "target_temperature": { "name": "Zieltemperatur" + }, + "server_status": { + "name": "Serverstatus" + }, + "server_version": { + "name": "TadoLocal Version" + }, + "api_limit": { + "name": "API Aufrufe Tageslimit" + }, + "api_remaining": { + "name": "Verbleibende API Aufrufe" + }, + "api_used": { + "name": "Verwendete API Aufrufe" } }, "binary_sensor": { + "bridge_connected": { + "name": "Verbindung zur Bridge" + }, + "cloud_api_enabled": { + "name": "Cloud API aktiviert" + }, + "cloud_api_authenticated": { + "name": "Cloud API authentifiziert" + }, "heating_active": { - "name": "Status" + "name": "Heizung aktiv" + }, + "cooling_active": { + "name": "Kühlung aktiv" }, "battery_low": { "name": "Batterie" } + }, + "text": { + "window_open_timeout": { + "name": "Fenster offen Timeout" + }, + "window_rest_timeout": { + "name": "Fenster offen Ruhe Timeout" + } + }, + "services": { + "resume_all_schedules": { + "description": "Tado anweisen, den intelligenten Zeitplan für alle Zonen fortzusetzen.", + "name": "Zeitplan für alle Zonen fortsetzen", + "fields": { + "persistent": { + "description": "Ob die Änderung dauerhaft (an Cloud senden) oder temporär (bis zur nächsten Zeitplanänderung) sein soll.", + "name": "Dauerhaft (oder temporär)" + } + } + }, + "turn_off_all_zones": { + "description": "Tado anweisen, den intelligenten Zeitplan für alle Zonen zu stoppen und in den Frostschutzmodus zu wechseln.", + "name": "Heizung für alle Zonen ausschalten", + "fields": { + "persistent": { + "description": "Ob die Änderung dauerhaft (an Cloud senden) oder temporär (bis zur nächsten Zeitplanänderung) sein soll.", + "name": "Dauerhaft (oder temporär)" + } + } + } } } -} +} \ No newline at end of file diff --git a/custom_components/tado_local/translations/en.json b/custom_components/tado_local/translations/en.json index 2383068..16785a9 100644 --- a/custom_components/tado_local/translations/en.json +++ b/custom_components/tado_local/translations/en.json @@ -3,20 +3,27 @@ "step": { "user": { "title": "Tado Local", - "description": "Connect to Tado Local API.", + "description": "Connect to the Tado Local API.", "data": { "ip_address": "IP Address", "port": "Port", "update_interval": "Update Interval (seconds)" } + }, + "confirm": { + "title": "Tado Local Discovered", + "description": "A Tado Local server was found at {ip}:{port}. Do you want to set it up?" } }, "error": { "cannot_connect": "Failed to connect.", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "not_tado_local": "The discovered device is not a Tado Local server" }, "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect to the discovered server", + "not_tado_local": "The discovered device is not a Tado Local server" } }, "options": { @@ -72,7 +79,10 @@ "name": "Cloud API authentication" }, "heating_active": { - "name": "Status" + "name": "Heating" + }, + "cooling_active": { + "name": "Cooling" }, "battery_low": { "name": "Battery" @@ -89,12 +99,24 @@ }, "services": { "resume_all_schedules": { - "description": "Instruct Tado to resume to the smart schedule for all zones.", - "name": "Resume schedule for all zones" + "name": "Resume Schedule for All Zones", + "description": "Instruct Tado to resume the smart schedule for all zones.", + "fields": { + "persistent": { + "name": "Persistent (or temporary)", + "description": "Whether to make the change persistent (=true, send to Cloud) or temporary (=false) until the next schedule change." + } + } }, "turn_off_all_zones": { - "description": "Instruct Tado to stop the smart schedule for all zones, add go to frost protection.", - "name": "Turn off heating for all zones" + "name": "Turn Off Heating for All Zones", + "description": "Instruct Tado to stop the smart schedule for all zones and switch to frost protection.", + "fields": { + "persistent": { + "name": "Persistent (or temporary)", + "description": "Whether to make the change persistent (=true, send to Cloud) or temporary (=false) until the next schedule change." + } + } } } -} \ No newline at end of file +} diff --git a/custom_components/tado_local/translations/it.json b/custom_components/tado_local/translations/it.json index c59e820..0cabb97 100644 --- a/custom_components/tado_local/translations/it.json +++ b/custom_components/tado_local/translations/it.json @@ -9,14 +9,21 @@ "port": "Porta", "update_interval": "Intervallo di aggiornamento (secondi)" } + }, + "confirm": { + "title": "Tado Local rilevato", + "description": "È stato trovato un server Tado Local all'indirizzo {ip}:{port}. Vuoi configurarlo?" } }, "error": { "cannot_connect": "Impossibile connettersi.", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "not_tado_local": "Nessun servizio 'Tado Local' trovato all'indirizzo IP e porta forniti" }, "abort": { - "already_configured": "Dispositivo già configurato" + "already_configured": "Dispositivo già configurato", + "cannot_connect": "Impossibile connettersi al server trovato", + "not_tado_local": "Il dispositivo trovato non è un server Tado Local" } }, "options": { @@ -72,7 +79,10 @@ "name": "API cloud autenticata" }, "heating_active": { - "name": "Stato" + "name": "Riscaldamento" + }, + "cooling_active": { + "name": "Raffreddamento" }, "battery_low": { "name": "Batteria" @@ -90,11 +100,23 @@ "services": { "resume_all_schedules": { "description": "Chiedi a Tado di riprendere la programmazione intelligente per tutte le zone.", - "name": "Riprendi il programma per tutte le zone" + "name": "Riprendi il programma per tutte le zone", + "fields": { + "persistent": { + "description": "Se rendere la modifica persistente (inviare al Cloud) o temporanea (fino alla prossima modifica del programma).", + "name": "Persistente (o temporaneo)" + } + } }, "turn_off_all_zones": { "description": "Chiedi a Tado di interrompere la programmazione intelligente per tutte le zone e di attivare la protezione antigelo.", - "name": "Spegnere il riscaldamento per tutte le zone" + "name": "Spegnere il riscaldamento per tutte le zone", + "fields": { + "persistent": { + "description": "Se rendere la modifica persistente (inviare al Cloud) o temporanea (fino alla prossima modifica del programma).", + "name": "Persistente (o temporaneo)" + } + } } } } \ No newline at end of file diff --git a/custom_components/tado_local/translations/nl.json b/custom_components/tado_local/translations/nl.json index 41a5edb..845f89e 100644 --- a/custom_components/tado_local/translations/nl.json +++ b/custom_components/tado_local/translations/nl.json @@ -9,14 +9,21 @@ "port": "Poort", "update_interval": "Ververs interval (seconden)" } + }, + "confirm": { + "title": "Tado Local Gevonden", + "description": "Er is een Tado Local server gevonden op {ip}:{port}. Wil je deze instellen?" } }, "error": { "cannot_connect": "Kan niet verbinden.", - "unknown": "Onverwachte fout" + "unknown": "Onverwachte fout", + "not_tado_local": "Geen 'Tado Local' service gevonden op het opgegeven IP adres en poort" }, "abort": { - "already_configured": "Dit apparaat is al geconfigureerd" + "already_configured": "Dit apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken met de gevonden server", + "not_tado_local": "Het gevonden apparaat is geen Tado Local server" } }, "options": { @@ -74,8 +81,14 @@ "heating_active": { "name": "Verwarmen" }, + "cooling_active": { + "name": "Koelen" + }, "battery_low": { "name": "Batterij" + }, + "open_window": { + "name": "Raam" } }, "text": { @@ -87,14 +100,31 @@ } } }, + "platform": { + "sensor": {}, + "binary_sensor": {}, + "text": {} + }, "services": { "resume_all_schedules": { "description": "Geef Tado de opdracht om het schakel schema voor alle ruimtes te hervatten.", - "name": "Activeer schakel programma voor alle ruimtes " + "name": "Activeer schakel programma voor alle ruimtes", + "fields": { + "persistent": { + "description": "Of de wijziging persistent moet zijn (=aan naar Cloud sturen) of tijdelijk (=uit) tot de volgende schema wijziging.", + "name": "Permanent (of tijdelijk)" + } + } }, "turn_off_all_zones": { "description": "Geef Tado de opdracht om het slimme schema voor alle ruimtes te stoppen en over te schakelen naar vorstbeveiliging.", - "name": "Zet voor alle ruimte de verwarming uit" + "name": "Zet voor alle ruimte de verwarming uit", + "fields": { + "persistent": { + "description": "Of de wijziging persistent moet zijn (=aan naar Cloud sturen) of tijdelijk (=uit) tot de volgende schema wijziging.", + "name": "Permanent (of tijdelijk)" + } + } } } } \ No newline at end of file From c9df491164783105ead1bf88aecd20cb016ccfd5 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 22 Apr 2026 21:58:46 +0200 Subject: [PATCH 2/3] removed temp zip --- custom_components/tado_local.zip | Bin 22481 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 custom_components/tado_local.zip diff --git a/custom_components/tado_local.zip b/custom_components/tado_local.zip deleted file mode 100644 index 83a36ee36b6f8014d1d5705ab46026237d64e27d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22481 zcmeFZW00iXw(ncEZFSkUZQE5{%Chb1vTfV8(d90y%eL*h{jRgu+3Q_*uN!f`-47=- z^2v4hsYf z_Sb*^xTvYZ13`YUv;hQwe*z39odAmeSO^LT3dp9``ELv1{(T{1fUS|Ehk=u+t&^Q2 zy}if(v@k<+H^39O_a6&iXn!fJaiV?Uhkx~t`vRKgY~SjY6*QzFlU>D(H%fR|wa6XH z#ZK?>Acu%QpS(U(xTWS@>n?1|gaQ|QuU>pUc$Y)yQp)2frD|)jd--|O!kq&g<4Mme z8c9hC!?0nn=5y)Bc=Z$$OlcE;Ovooi1YX0LLD-fA;81cMFrVo%$q(^OQBEuGkTxM8 zBJqxRUKZxyIE?N4bn-h0+i8i0LQ97Jt~27xk)l|a#6gj&RdMJ|@s&>`L)W4(7#0jE zWJPD9rP9Db&=3*VpYLFCjqX?Mw`kc_E9`4Ab*TW~GZo&aMTZ^X5BuItDv6gs+DBsGLA9HfHljG-zAupg%03uQ03UctIG;&r9xBMV{D3Y}#JmZZM zUvE2c_UOjGXnz~=sI;^YcEL?+AwZ13j|QDCic3o|N<>Vu!HXb2Z7R+;sBFT!e8Xug zBj~W(@vCCBY{(l za4w7#pNJI?$qR&?>L>T<#~kO;S@BXuq2G55Keh$mgnPCt)9q;Rl=BD^)tBBp2hfLsB7g&?1XC+5T`Xghgl& z&S=uk;38q}d#V{&s8H&>$tu`Nh@T+9#$Aei9ZGyofz9W>0nl`rYTOY$M+Yx3^LUW4E`X$&&9uaLWy;(QWkisfL|;GP(w`o7I`M6qm<4KfOz;_YnQ zChbMMw&hXJuohF4@o{r>cHu2%2!j`sN7pB?dk&th>4fVwA?lXjJ$j4sJnh+WWJeO` zi9|p6VIcNkd3kfQqDuGf!f_tqR*E3|)EX9{G9SqCak4FsDs9;Y$Lmx2=HbMf_^6K_ ziWti47JxhyAIBq_4TiHLc52bUj;WW#qJRWUa`LeYrLxLND47Z|_7Y$O#huACz2!vq z+TSiVd6(`*RAgA`y%se0-3t19=?Ng&#@WY}=yA#7H~x|31Z$1HvY&$q)E zjxkZn6Jxql=hNE(>k6iU1>)nG4tQ9PW?HivpN4HZGGrWH6DeQdtI0Gh{aCYVfy-w@ z4I&2tK<5_{mlu=wfq~$J9_%ME;}bG2>0hIW`(qNG%raa(`A-q9wrB&Q{WS1d zdKI%qlbnfIt;`vdt7Yo`nriCD!6Xy;R;>f_{m1q0z^c34nlgq=$BLu3j6Z$?;bPo& zaj|u&$2~{}>qakOH_S}lOyYgBz(eJc0&3E4i*&uSLhe+1=DsepXyi)v4rs9L@drguw%uxwo|?&|cv zTvOAjs!=|<>%0`%2pO@Rsi7S(PPPLQB>}xbq-!0VBL=CICV(NVJYRk5woIx}g&nv{ zKYb5`$)ms6tSGWXErok{bY?eLzz`{tLh|H|#nqi+&brNL)KgfZzFJ9JY%J%pY8Q)O zEqd-aSC6%HOwyv#WSx0HkpA`+^9}kxC-S%t@IY7U)P!u%` z6`RfU&hYtP0yo`z+X4A3gXKL$mp-{%LPJ@?PKKRx;2XR2L{U&28T7z= zE(=ocym_HH zQAOs1<-ftr*>r#m4=#uezEii(tXJdmt;V7-d2pQwBLgNrweQg_ftRPHu5>ljX2_g_ zVY7FtJItgU%@)drzp>&kwBBcJQ&-oDRYTLK`@%`U^3}&>W`J;e#+b=CvD0XFF_Wde z49kmwSWu0DSK^D(ml=3;+~aA=i;`~M8<%B30d$PUK*h$MbJ;JyyKIb^Z0}v#jMZUA zmrK?SFR{Yjn@*)XN_adjKI0#3F<6$Nn`5jvD)o&2l%cB)p94^|*^&vYhr>13f^~It%>~2!sf$d4X z_#Cs^FZ@XumR*^gK+Fke18)Pxg4^3_#}Y#}1ulhnoP(j=F}nV3;DZ4MQ+YB6zoUar zxmJmH(g&6js6GB%*#i_wwPx3Vl}NDs3IP-hPv!Z5%5eJwc8~!Cc6|RLukO&Vz!$+* zwb!KTZAnAjA(mAD7BIfJdwpo2C<6&aOcLK73JC;MLIVW!-|>FU!|8AF9{um}{<}56 z#>m<9KScU1ur7etKO?=Cf!$skvL9mHRUb7a5q{I=dAo2zxw-d}WI4P+GLZ07ikHuw##*tarQ*h?KEQQw_{HO_}zs&~cf4yKY zPbZn{P(2-)*N?i2@?RMxrgb~Ko+r`}jNh>|_Wp8>I^>!rlRF@q^rts1pM*3Ta8qqa za4H}yE#JR=iSDXPI&4VV&dEi7UVk^`ZJ!`HJR1pFw-Prc9N)Nwj6 zbKtP4nZ$I`y@{b*S~Tv`rVCoUznpjG(&wXjdcI%>P4}jX+^0S0*^vEEGBVE!qX(=&x_uB)F%Je90m|Gv7rfMvae*&5SK?27z{9z%(3;VF zBfpZ4h>p@$-5e!M8k5h;5oB!FTlgfgb?9pW{>ZE`jF8konDOVbzyu?HUk~{Y*g`{? zQbIBWi?xl?Hqf4yjLc|FXCAxwja?0AEq_H7q9Et*n zDAy3Q+RDlL$0o~4q%nxJIft`;I}vCcJLP0Q+Xnb;1fRT8#4jVnf7@Wp4LuJw$J`}D z^)e6KNoRMu?W72x{d&<#^Cy&&@T7SCAlG+)sHPJV>358i@!PXd9+HYzzIB4|Q)E<1 zcmTJo@t39oY3uLXhaVuuNN|Uv_GDm!3Cp<>I#Y+MXVfSBY z%`3k%jvX5+AX=gf(>Pz!n1xIVT6BLw-kt_wR(5*dgPI<6B%rOS-IC_=OP=z--q^LQ z0Cd-QoI0}Nl&v)9cLm(D7ilQP8X|$=brFEsP9>~RM2`;Q%O?VT+l&yTN$BtPXW7h4 zFuw7^z#b(4!y1k2$}6R7lP6no;tizS_&uE6!g&CMsv9FhA7&{V>A?(-y=DW?g}@UI#eF#P$e+~@PylnjXhQYK zX^a(euD8uQPsADKuP#jNeV`U3Q|WZ~T|icORRb!7|E;u5BDOW^HfeZ(3gs$qkqg~W ztUn2z@@;duKoO|1)nq=8L&$!mP7BOw;I=K!&O!Gxoh$mxpzVF@i7P9eF%Ah1be
lM^qY<2`Yy=+W#DEiMLhoYI6h*N(moCPeWg{UZ)C>sCrCI3HL^ zvZ8p)gqz&&vRksX_|uqN4XaL+iaCN-ufA%1+dv#&A|`$OBV z3+Qo@$e=J(7)2+E+wRh6{Xzvk_7}+g_RKjS z?9+CO7dIv1=w&i~oZ|LAJ5`dN&d>~So^uAr275d-vRB`9GkEhy(+~eNujfu~9 zWnqh2>sjlXQzwz}`V>GXdFYwG?v;DDSPiqO;(2#E=8=rg;sr85_Fu9o7}{3Bz@U*v zy@j+kRo0nN{ge}%L(mVB&O~F49DUU~0_B$QVm)?mQa{1HtL_090GL*(k~qOx)OT_7 z>ecgtk_!+`V@N_D7Ajmm+@q1^br7sCzs&4_-j(~3X&!$Fr&_Ks@;@+G6GTZ?2G<=_ zx7hpW0#d^nR0Y$O5>8ZxzoAp7aV^*~LTxBYW_AB&xd?^mhw#)Bd@ImFLyu*Zy6wq$ zZn-SNe=jv#7Wl1^CvmXyvWk~$uvv-m;bXP#i|AN|Eh9!6<99&?Yw&rRn$Nhxsf;#b z&?-4jYDA?+L0fSg6PjchrCRM`3}cW&!q6hcjf4}UxDXiOH;qAhG}H9nafo!qcZiZ z0%u3T3+n?hU|eS8y@5Okb58crL)b}x8>9ISJ5-J8JYx0UTC2<;9d;J}Bu>TEo>}<^d<_2cgc5eRx zb~Yfp0N(!vJBwO2c5CcNpZa-U0V@2eAiAyi+Hj29M&cJ$z^j~f^9VYMP*I|5)}~5i z<<9bPUkzUk0xdiV4f?W2#ddx7{Vk%KH|||tm$T`$u*7WbNLpIQ%1IH5!ET6z_B_Jv zngG3pC(A^N`a>&n>5uDv;Rv?b%)ld=U9+fLx!5R=0xuJ73wlSG)%D3_O_mBYjf}$% z6iLyZ?TNGlZO0OCmbdV@s*Fz{7o2b!Vam%`_+P3Kn)?S~!nAK{$9Aa|BehAFqiv zCH8M>_ciye4K<(xx3iVR8YINgso0)Fk>86vs3p@?c;mAIjN{INexM(KV`s2Uap$<( zjYRKdu-H@Bp8I#Hp{b&#lm>6xKVs|9r=t}aCS1d%XspW9nL=n|0E=&Ik0sLR>mt@D zK0TmnU;D3UJH?oDD(^YW>DN3aHU#$$7iv@kVAdTZ5OSo=BxN zPoN0=fw}D=+D*t&r<^xJyQ*5heR6pC;AGn5L62S`@V}TxfxGG489lZU36?s8{gMl?xSc1 z8en`INI@GmQ?Z>SeBOl-7u~P-3pCLf-q^uxNOnlTJI(}Y4QvtHh5??_a3I7c>v;Ja z(yOtY2Y=HG&SL#1&kuAB=s_eMlVat-qIF<-suMEtHt7CUZ`^D3!Ok&jUsgVdC*Dee zj}VIfM7DXF_znp+upPRR%4lkP71~L&clrxh0rTNjr zs6(6`7^H=A@9Rhz{d`4!ACXql5Hg(&=@p~HDNvTfox%`DoPpX4$!l>OPflxI`>fgO+Y*=DK(AkIX$78f}G6- z5PE0EbNsVr=jdn(Nk{JqDx9jks}dOQYmgH~Xv%Ctc>MOiD$h;@bupG&P->lO>h43?cNo;sG#Y4XRz@tHDz zyD!W<3;MoSbVHC zsP;>|L@Cc41W;$I87pp^mFEp7VBRK!=9sJSP(L{{m3sti!e0jR`G@a-nH!0L9Wq5G z*~l3XBbYWaqH1;|e^`famiG2M=){Kt4P*GHP9wC(s`9vr<*eyuM@k>EBBWc6 z(n^IBj;3=z=b{2ZZ>Nl~XK7lJjj8Il6{pvK4$`lQ@@RWIF{F25?%a>MjT**sUUGTY z9|U`aCG7>f?f&?rWy-?>DFdFsbwyqUEp|6bX>#Ayk2$s$4Pf{l;l|{d%B!Bxs#^Z$ zRrs@$w9|i&9H3>w8xUt@IHaLg?WIu($}ADGEXgg40=6+Frfe5Q>eO-iU03j6$mPhc?#>s{F>oEH3i?VUK!Hj~yn_~_(~BR>0k`syix z*!|_`Mg6WDkU#Gz8E~~&2tQ)4W`br<-~|`pfLMc06m!l!i8~fgocKv;2SJZk82@T9 z71g)ql}wX|>!rB~GDmOdMz-}-60Z-V#XDB7&e_qUx)oze(OxL1Z7N4aKs)2 zS|^;=R1p4&lV@9u(E{w2fCB6|Z~HRl&gK=@IO}ISC^MfBUUuT(^1MLLAV_w})URKV z)|aei&#PS6WGjJI%?`XD2_3|!A{~zSBM9|6)iLss3)L;Qum~JqBRQkM*mSA7K=jLe zy*B48@2Eg)Qcss=D+Q=+8oNeTx%j(poXBT^gS$wTXuj!T2gj_$T>09m+dU z68%~;$DVkl1d|ij33OE%wG@O*ap!ll?1RaLr5D(iHBzOV@fT`^#!id=`*QkZiLvUo zP!X+nD$yBpM)znUKvrb1E;2V*fdXd{5r_JE*1kSzVRT*yqCr6#h$CB*DQ<=*d*swYn8H#F)3FByW zqBC*oEV<+dHUg3N7u9B9nj4Zgmq=pG!k4JV4S{>js`|LcsMopj5+ku1F|1WrV+TWw zaTw3J^0}%MwF%iE7@_u4mB?lSLVhUpkK)9q1@?G-pl4~H-zqEPsh3;sG7MAp;50hK z(*ZDEo91Br%y{nyyfVB%(<&an$vGTPZ{9h2F5rHoHG}Klh{8{RcfhU=KxI;O#Q<;m zg)B2gO(TY|ogg{$@_Mu1Ihy`Zvz2mY9WWp#0H^;gV)8g;8h%gz|MCFrU1(oZ=-_Q~xpvU51CTdL@bk9#K- zj4JJy-m~ESg#7E6+7G4EiGu(EG5;yr{ddOBrq%p!8M}W2SrdSfxvia(GvK?^e>kkY zxmEut{8d4E0DM&CBG(v^y5BV5I;!xyxSSQNI)Y~~w2xbg@6DoSRvl@VQUXa!&*)#S z@W%)%kxDx>vAdtl4gqMwqYBkA1-lT`{g2AnG}55WbJv+_Fg@i7%{s!8Xp7n|f`E-R zeJ5!lXUJkYMyg~}bxHtv6h4CR<UWt1l_f!<}Ff*XFc+Pv+yFYQmy8$7kc}0c3 z8bk9`^zFDXmx(iqG?c}a|MPy&;pxE(jfrIxg<3yoYnHE-6$#H`#$(-Z?z}4xjh7bo zj?DfI7@x+*wn| z5hO=@)Ia-~kA6Hs^ac={*206V0mCJZzDyVy4aPTjcP=+|yHmM3lX5{$;;GAHiqXi!e3#>xs^ioka^B~} zphw$3QO;YwHM0?2iPM;#IeLU`aCu}8c~+5KwsFqU#$St*G8LbYWj(;A>GTPHeUQ)y zx6Pv+dIaRldtk{s1MON#zvXZmLqMCJ!eEDQ{R1&-ZnXdr-LF6NacZQkDn*jQ#5)%v za4emH&EsK*7xqWQJPkVtZrE0m?V#)!z?kkx%YkH8;IEULES8U<^5^8%ga7NvmCE4x z+sXa6Tr=RiovjnSrIVfQ|5R1B1oi;HjdcV5`mQ`YP*zl=mZzhhkd>!XZ%{6SfCdVV zP>z&wQrSawa@>tfWKmX;c8V)T_VhQFZl_9D9}DLy_n-lovD8T&1bWZKNFI4><7 zdAoZzKi7Atc5t-AQAx=}#!^q(D~VYKYK9U9f+7g^$L=7Z9Ir2-tp3!_8~=R%+YTRs z{$6s&`g^#rF|q}inf{3w|LKMdNZJ5C|D`9RUNOF}1?% zpAo<{L1e0iZhJ!y>YPofi##T$rZyjWhM&QNI!6_v3UdTK7+3E^B$Oa!wyS|3JAfg` zVV5nO0M8%qNP9njo$l_es!lEvi2IlKi!U7K9nprBXBB1P(?5t{K`Do-%EqBJ{cIq?;M&V~Xbhq6JlRP}k};G6Njeu8SSlbv(t z>-Giu*N%SM^prXOvzvp!|GJ|mK7Zd)w7>7@|7X*|6J{GA;Gd-k?KRsyb|fFfI6tBV zOj!G<4SpO+rDcak2uCf_M-ZDjBCJTqu+ah;CFm}p&*HB+!n13V@FY{78}fjsUC61XHhxsy+Q+Gn_pYiu&DVP+}i-|6;3$Slr;+fude4S_h6DjDNd ziLP}Q`&tfqiE5G(mcwtXr13V*v}GXMSG3} zvE!7==8Syf&QuXHp|`+I&we9v8vj|Kv<@%l&wsXm2Q_;A!wI=QX>>frx7lRBTA9X7 z1+E7v$_%|$8@vU+Q-uy|8kOWby&1R{L&SXwQN2RKeuk{=(^D1E7P7=qY4?s6ZuaBU zu;1fr;myzOg6Je#lA981?Ab7|>?1sr| z?n_9&5;o22x+QoI+;p?PX1?c8fVPY_r!~Iq^;?q!|Hdfj#%L2R_a5gDqcsQ8Bxq8Db4xfGH0}VTu=!z-FmZpNkT*Aa* zz)aRR7%JJh0TsuuIm$u2P>ne%>EM2u+SelEqdUC9tIU6bkhBm2vbq>%;gQ(8afX+EuLB}!%nsc4eOSy1zhsL8)_+j6p-IK{))vyS@Tz&f5@f@%pf{* zCVY}MbVJ9e)Wlg6ua?>Yzly95>XFV=tsId6jJSbn+M^@|7hDU~a^JGqka4Sb8XS@= zaz?{UTepGra8-Gk1ei>r=(rydT2aIUVTQaRFOoo841cKuw2cpTaKtrQuX3@v(x5aW zo;7w%^nn$mtQ*IGLams%=7j}#YC;$(D}EUY z)_)sy!!=S~Ydr%#w0KEjI<#FwGTU^&yB}^0Ym;TEIMtB1ownj$Ca8*!nfG>u&yJI^ z7Lz+yLqvLW6UZ(!x_Ul;cVs>9w1&$dA#=y5zhpy3wMYfIU(l?Ve5u%Mwcsjr6dih~ zZ?+Cj$>DhD8y1tB^FDf+HybY=F|f+FEZ8&5+AMkJr`v5&y=S_{&JBgC+~Lk&tmP4> zg$`4C67wzW7$`QiJ4xSK=k(;Eb$Jw1=p8o{x|DnCJ~YwT0;-BY#gdDtjG>4n32DU> zm+q~>lh_s9s^M!J`NZnFS5@YwtV+~~fy_CL6EE`xee`~CeF9(ucz>R7vH0(J3A=*%O6kw@KaY+3zf*;z4u6;B=p zyPFk*o?Fy)P|~;;y)9pI?-)!I(+G`5&Df1y;;SpG(8}3b^Gkn-k&Z@p2zL$Pa-LN( zyrW86kSPnRhD}fcuP*%3E`b}(B*@p9@Z*%l(_EU|D4QIDJ3VU393fc4QnEmP=fn)% z?#4(BS0+sF^R$b_kA|8&Ef=-qdj%gIZs;{k8*Kkc;|QV=4&>Cqe^PeC;}2v$}|1 zPg`8?i(~JiJz~CD_6;&C(=5!zuffc1>fgTn{)*JU`YdfF|48M^zjY0C8ae)rvoh;(oR+IIaXhESD#h6TC!V z&_~mrkR>DX+5K&zb>rbu1Zr_`yv1xPiz_2Og~>JawO)2cu!MCzQnkNXS`<90ey?z^ zP_~lIK8LMkc9bjWOd9|zENvQ0m&`g<@upqQ%V5gi6{(EAv;-=jzeE{D6^%Y89zM6S z*N>>peU*waQ{RE4#&)J-s8-0Xq*6LSqL4HPpb9R=;~ugbhzNCkFxJIOQ?v)18+yH+ zy&pyE>@T-!Xr%TU{XRlDu&6yjcIj(JvzjAv4jJeg1yi2P^Vx;4QJJsBWZ~oo!2w5( zX;sOzk3Q0(Hzb3*71v^7;RLn+6+|6S}y_kNUbZ}fruxqXMS8&@#V20R**ts&sY?W zY$l#qzR}4XQu@=q2rOwS9aYq95!DY}LjlJv=a(y5&-n^yru_P7>C zR*+ut6v3-9Lyi3avoqM0gHi9b_W3P*zFwe>jT@&NDW6=Nt8EcoQZhb-P0B%8xJgoA zV)K1_9;GKM`=Z{hkmdxcS)JQ3)mHeLXEQ|xjv|CkkT-lT<{Bf{1}WstfkWe(tABOC zlt(Fr9y@|_&?*Ps2pQ-Ci|FotGT$?YyUbtO(w?VF7p?87J}e2*ciUXH3bOhRI-`*!?NO(O&fK z;tv6^+=fSEpR9Gd_}IBMJzOQf3dG@d(53#cGrMZ=x_0RT-N*YdNT@ALON~LM8D|@) z1}_!%24xcb+VON5$HN{ki|!IXMTjMllSzt3^;j&trPDmD%uf~&=gqHw=!xm(RE6t5 z>PQy)U+EXJ()MpQ^>2E@$@IT$%EQRU`hOBgSA)&}TOeI13Q2=8A$2jF6Grg06Y(4N zB?=(*?;%kn%P}TKWg}GeY|1Wap?e4VoE21bbTmohG&&WAJG_sEo32~|O)yJovhK5P zUoS{nkhG zc5Qt~))0I=K%SOSARr~Y={g+Q76>wl) z{i7ay{_Np@>oKL8zo+jo|IH^kI|6LY|FgctY3v3B{Zl}w>qVmSq4i+CBZjs+8vQ0D z3ajc2gDr@xfx42Dq%dDKB*(t3Z>E3S+DbGjky{&62k(c?FV5Z{OFZ(P9=tOLlCyHA zNc|<_9v0iQ&5O_}p$&y#(=I5Z?-|^*wC-TE-3@5BoQm`bsA+b*o;wcfVAlSA){lEl z^J9a!t*g6zMPQ6?t2YYcpsx)HBRkTXqt%{^Kl`d5;=2#n`ta6AH6s!eb2;Ct7m7?7 z0|44~n9O}A&YO5$tC8o0^TTI-&NTG*XOqOXcE{3sq1a856TOUIT;&N+ReR7B7@fbD zd)(mOM5x939=I;0_WH?8&^=Pfvd&`Rp_D_=QbzYP8{P7%`_f^VD7;;}DkaHI zn^lK*jSHD*ODUV*G3(v23w*SWcc|WHqedcDFTh5ojQYKuZS~(iA3F z4$qFR*1qwe<5bA8X2+4I4DY2#z27H|x&pLz8A0&}G|!#_dkYg4?z)N^W%GS=R%ga} zur}Xkv6{;{^n%G^i8Gj`TU>ADj7)v#c>Ipf?Qkc+tHhlOB!Q+xq?gmua{gr+8kCUP ztOgX!N{Zrc1<|6NVnn0t5@rZ4{>U|4;Af)0eIR`)rc(}5jjgUVjrp;l@G*_-OR~?j;}8_}Aaz%qh3+hM9Iv#=yj-IQ z7VLFkfF=btaD*&ci-kd-9QYMATyR0ylfQ0pWRrK3HkV>%(!&@nz3g<>pBz;L>MJ^Y zOIDCrHe{rNV|E8$sPCZ5?nMTUeEWHQrtpQlL{r$ z{~U2X;FK}J-3?Jh!Wnc+FyO4q#Tz8ZmXCtm4Ter`z8YD2xW;}FxH##o2czaVYn^rp z1wq(}FpTqh!O3M8lBfhCKit`Te?gsdQk5@?S9P<{7xrv`G=by&8O8s<@&k2a0%bm+ zKtNpZ|7zis#u9(?kSKq*aA#BZ|CFESBMkjV$}?Rw1aP6T@mKBM@5!Ja8jBjZBE!}T zE2>>{+f2)!6+6Kh6ax*6F59{Wr956Wi+19x=i7tWOX0N)hjk=@bwt|Ld%m{O0=4<^+MR4+ZP9&dySE4T|7PRUxPRAhaZb$YS!PinMGvMg_zQ3?k48v z6KR;wgfnRSv-VI_CCd%T6mjIpWd*{LOyW>m3vguh+OK(kpaxFEBroZM} zJ>ue}n4)Q`lysg)P#}A}gyj+Fb;(a&>Zv0IJ;r@$& zBy|7-{M*!wS*45da%*%g|2nq|>XVGeV?Xj+I+IgXYd|X|t14_sz*5DrR7dn<*Ift; zVz_Phg}@AN=TSa)vo+&DUp54jqiD=(hUF6!CcOC`Hlcu#8aN5;#lc5#q-+ht%|}Qo zkrk?^v2iHU)2hTQu%<@dFC$GaVSHEEB1u>{kl)qEOW;x69mo&R4&cck(P@+RRAG}$ z9RBO|1rTp@+bH3ELqv|jGd|>sW>EzfVlr!v0r^#=50wT)o_9s#Y#xp*Q`*AUb^pQasM4)ES_$`g6UpGnZU zUgkkq;Uni>)Y?IeZ=975fuQ(Rh(jJ-tJwlHuErKT(dCB#Cg#3 zfr_iCXSW2!PLA9nV#DA6t}`_V|4~TM!I1&lq+(Hf^{!F{+Mm+ewK`81y6tB`g;rhX zDqK+72(s7a9PD)RkL=4+Psnfcv63)e$l4*xZ^R%jT>qS zclncW0|~)C6FQ2U&?CCwKxi-2Q6LAMmtiCtv{Zda+{(^}&w#1^NB{D-)&gSTj7R^o zD28_E!g~A&2l#z+8OMSoRU?8d=zT%Runt{+5(WbNzWGRaju+!L#{9yFQMdgAt20Bp zGx(RowZfpE>|FCQ(b@2@j8=Uq_Zf`wTAT{$zOz&2QV@}z*d4Rx-Lq45SxDc;QrVMn zE<7(wNLO}QsZkYbbuGJB%iL;l_Ayit!IUth?tnn~9;9)~h;VgG}`8 zt4Nmp@AA=%evqa`U7DUt@G#gO6o)P|lO->Zi(2if_iEi8`PYx^N`!{k z>e2xsYXA~FSpPRP>UR)FKUX5*(7esH23Oo*F;!OjOE_yAkp}#`3<0>$r^DumMI408 zrKIT5yQW7sS*xkVTsLi8i8~KnFM{Pyn?3E^{^`BdBb;Rfta)p+&w3^s$$%rKi!Zhk zm#)O#fAFm@hs22fpUnRz=0CmnA6Nrg{I?Ocf8$$cMc4DpIgolFM|qIE!a+$r{B>bUYjg&w zELr6ddT7fI&gqHi+qV%4wL|QD&;&#j+E0GoI%v#&N>-AF7NiouX)R2Syu6t4^Yi+V zizaYoo{@Oa5bSE;vC6u9p+n{q3-I0PBPK(@R^ z_LC(rcyZ)WQr9Lnit|ly!Vvv;s?^n6+uM@^;!5(Q##T)@(L_W8SuXVy!zu#JE%0m{ zX5}_|I8qQQVu!Ey8&abe8u)=tl-zKutn4VRuXqK$UWz7dULCV!}mML#q9!EiWqZEH4 zhyq>YIr;rCJb1Vm_O_@Ud7?U?gxF19!yLcDhce80t^Icfe#)*eJD)$6Xmio`qLpG? zfsQQH<+(C0ww_hXoWfI=T(*?l7}M!Srw&_$p|@*fN$#fzgAXE9Z) z30;U>M`(-^9fG)RgvC9Hnqk@*3n7*cbvSOE7T6Z>iI!V!Hz?4X-`11LE5>9XILza0 zpD;%3HD3dJAP?mecM=ulCK{TIuvqT^{bp9ovB)+ZAFiz57^}ZMmR4^dlWLtU9H07k z@gPZtaw4`PdKA%<*RbRW-Kt8-2GW_$>}ul9t#D~}f*y16h81LlYKvjz0B6wBS)-m@ z&3}{CbFYS3rOQ=$f?s^lJ99~L90pGwJ|2@c;emX1YO?yCDzlWkmh6rq4?(|oZ0g8A z4t%`Cy?n-XoIqNrQ9fz^g_rO5tvg1lRfqt7hU-o)>FihPT-FaQ$zkD+kyZU)hFw;4 z_ujGTpEzf8Fz`;53yUTyOX%&45_c12(Q)XhM>A<_)fe%Mt#O)z?;>%2AqfZ$6wpKU zOW4sZ!hw~-VpQxdFzw&a;pCQw_p9fnqnc>u^4@4Hy3v(@qGscyd3A^I*==o#&|eN= zH-U%a+MJYhohV0fL`JN-e8gXyw(LrEc}C(5VtmTTZ^XwUaG?+Cc|T4Vvjs4O0VtWh zOc(r)l4F8BNnDXEU~@`6`Re^YA{jE<{P9gTBO8NlS3R0DORKCy#_V!h+!=PFLIwAt zJXlhug*GuiviE303b^8a?b&Hd2FeMAwPac?To|0~bBBfK!5_9Fa_?hiGjv;F(ZY1q zWWm(48jDfFrKkH|4f{nfh=AfjSHJac&HEx6$WG_-`GWtYJ6|cspV9x+;z1Dp>DT_6 znAQ0|@Ia=v|Iq`{|Ca~)D=BvIfA#18)t~=YfByeXfBv7a2H-yz{{P7ue+}MWX#BEW z<3#GgJmx{_AO*(o@Yj#9>3{?kGqP^zhH2tVfs}~{@yi+xxO&6lyY$-s$t|LVVJc(m zsB5UuGPd&vGY)ZIUp{zmVQFsObP({AuU34n~?b0yaQ4|AmE$Sc_mGMV(Lig zy{BBzTqh5*=doG4m`4X7DN0t?`Wny=*`K?5%R_MWL}1j@6)PijnO6lX9ox4y8rM%w zCm31j877kXj3;)lps*)dfV9zhWX5$|t-^@aEV5~vn}CvQI$Q*x6keBLDL#HIe;{{E zcT7g19% zD^+?iQuqaV<+TNCKNT~stvZK=J020KRxC=NvE&&`q2jA~8X{j!H*IFMt_9lRZ z48~p^MEA(3R;3@i@!840T@WQH$! zjBdmvfes7UOp84R7kcHG!!nO$3ED%?=ib@KqwstYteo2}{~HS3ts#g%R6Ð+K1+ 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 Date: Wed, 22 Apr 2026 22:06:57 +0200 Subject: [PATCH 3/3] Update README with changelog for v1.1.2 Added changelog for version 1.1.2 with new features and improvements. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 86c9d9e..69913fc 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,15 @@ This project is licensed under the MIT License. ## 📌 Changelog +### v1.1.2 +- **New**: Automatic detection and configuration for TadoLocalServer (zero‑config) + *Known limitation: `localhost` is not discoverable* +- **New**: Home Assistant notification when re‑authorization is required +- **New**: Air conditioning support +- **New**: Persistent Home/Away SMART Schedule handling (requires an API call) +- **Improved**: Sensitive information is now hidden in downloaded diagnostic data +- **Improved**: Home Assistant services now include an option to set SMART Schedule persistence (requires an API call) + ### v1.1.0 - **New**:Home Assistant services (stop all zones from heating, resume heating in all zones). - **New**:Server info panel.