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. diff --git a/custom_components/tado_local/__init__.py b/custom_components/tado_local/__init__.py index 7c06539..bb2aeba 100644 --- a/custom_components/tado_local/__init__.py +++ b/custom_components/tado_local/__init__.py @@ -11,6 +11,7 @@ DataUpdateCoordinator, UpdateFailed, ) +from homeassistant.components.persistent_notification import async_create from .services import async_setup_services from .const import DOMAIN, CONF_IP_ADDRESS, CONF_PORT, CONF_UPDATE_INTERVAL, PLATFORMS @@ -52,6 +53,28 @@ async def async_get_data(): raise UpdateFailed(f"Errore API Status: {resp_status.status}") status_info = await resp_status.json() + cloud_api = status_info.get("cloud_api", None) + if cloud_api: + if cloud_api.get("enabled", False): + if not cloud_api.get("authenticated", False): + if cloud_api.get("authentication_required", False): + _LOGGER.info(f"Cloud needs authentication: {cloud_api.get('message', '')}") + if cloud_api.get('user_code'): + async_create( + hass, + f"{cloud_api.get('message', '')}\nUsercode: {cloud_api.get('user_code')}, Expires in: {cloud_api.get('auth_expires_in')} seconds.", + title="Tado Local: Cloud Authentication Required", + notification_id="tado_local_cloud_auth", + ) + else: + async_create( + hass, + "Look in TadoLocal Sever log for details.", + title="Tado Local: Cloud Authentication Required", + notification_id="tado_local_cloud_auth", + ) + + zones_list = zones_json.get("zones", zones_json) if isinstance(zones_json, dict) else zones_json devices_list = devices_json.get("devices", devices_json) if isinstance(devices_json, dict) else devices_json diff --git a/custom_components/tado_local/binary_sensor.py b/custom_components/tado_local/binary_sensor.py index 4d88c57..8e5660c 100644 --- a/custom_components/tado_local/binary_sensor.py +++ b/custom_components/tado_local/binary_sensor.py @@ -1,4 +1,5 @@ import logging +from urllib.parse import urlparse from homeassistant.core import HomeAssistant from homeassistant.components.binary_sensor import ( BinarySensorEntity, @@ -8,6 +9,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.network import get_url from .const import DOMAIN, MANUFACTURER, format_model, MASTER_DEVICE @@ -19,12 +21,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e coordinator = data["coordinator"] base_url = data["base_url"] + # if the base_url contains localhost, replace it with the actual HA host to ensure connectivity from other devices + if "localhost" in base_url: + ha_host = urlparse(get_url(hass)).hostname + base_url = base_url.replace("localhost", ha_host) + entities = [] zones = coordinator.data.get("zones", []) for zone in zones: entities.append(TadoZoneHeating(coordinator, zone)) entities.append(TadoZoneOpenWindow(coordinator, zone)) + if zone.get("zone_type", "HEATING") == "AIR_CONDITIONING": + entities.append(TadoZoneCooling(coordinator, zone)) devices = coordinator.data.get("devices", []) for device in devices: @@ -74,6 +83,40 @@ def is_on(self): # cur_heating can be 0=off, 1=heating, 2=cooling return val == 1 return False + +class TadoZoneCooling(CoordinatorEntity, BinarySensorEntity): + _attr_has_entity_name = True + _attr_icon = "mdi:air-conditioner" + _attr_translation_key = "cooling_active" + + def __init__(self, coordinator, zone_data): + super().__init__(coordinator) + self._zone_id = zone_data.get("zone_id") or zone_data.get("id") + self._zone_name = zone_data.get("name") + self._attr_unique_id = f"tado_local_cooling_{self._zone_id}" + self._tado_zone_id = zone_data.get("tado_zone_id", "") + + @property + def device_info(self): + return { + "configuration_url": f"https://app.tado.com/en/main/home/zoneV2/{self._tado_zone_id}/", + "identifiers": {(DOMAIN, "zone", self._zone_id)}, + "name": self._zone_name, + "manufacturer": MANUFACTURER, + "model": format_model("zone_control"), + } + + @property + def is_on(self): + zones = self.coordinator.data.get("zones", []) + for zone in zones: + zid = zone.get("zone_id") or zone.get("id") + if zid == self._zone_id: + state = zone.get("state", zone) + val = state.get("cur_heating", 0) + # cur_heating can be 0=off, 1=heating, 2=cooling + return val == 2 + return False class TadoZoneOpenWindow(CoordinatorEntity, BinarySensorEntity): @@ -129,7 +172,9 @@ def __init__(self, coordinator, device_data): via_device = (DOMAIN, "zone", zone_id) raw_model = device_data.get("device_type", "Device") - raw_model += (" " + device_data.get("model")) or "" + model = device_data.get("model") + if model: + raw_model += " " + model sw_version = device_data.get("firmware_version", None) self._device_info_data = { diff --git a/custom_components/tado_local/climate.py b/custom_components/tado_local/climate.py index 2ca8484..f775ee5 100644 --- a/custom_components/tado_local/climate.py +++ b/custom_components/tado_local/climate.py @@ -6,6 +6,9 @@ from homeassistant.components.climate.const import ( HVACMode, ClimateEntityFeature, + PRESET_AWAY, + PRESET_HOME, + PRESET_NONE, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -47,9 +50,10 @@ class TadoLocalClimate(CoordinatorEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.PRESET_MODE ) - _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF, HVACMode.AUTO] + _attr_preset_modes = [PRESET_HOME, PRESET_AWAY, PRESET_NONE] def __init__(self, coordinator, initial_data, base_url): super().__init__(coordinator) @@ -58,6 +62,14 @@ def __init__(self, coordinator, initial_data, base_url): self._device_name = initial_data.get("name", f"Zone {self._zone_id}") self._attr_unique_id = f"tado_local_zone_{self._zone_id}" self._base_url = base_url + self._attr_preset_mode = PRESET_NONE + self._update_preset_count = 0 + + self._can_cool = (initial_data.get("zone_type", "HEATING") == "AIR_CONDITIONING") + if self._can_cool: + self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF, HVACMode.AUTO] + else: + self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF, HVACMode.AUTO] @property def device_info(self): @@ -90,24 +102,78 @@ def target_temperature(self): @property def hvac_mode(self) -> 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