Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions custom_components/tado_local/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
UpdateFailed,
)

from .services import async_setup_services
from .const import DOMAIN, CONF_IP_ADDRESS, CONF_PORT, CONF_UPDATE_INTERVAL, PLATFORMS

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -45,12 +46,19 @@ async def async_get_data():
raise UpdateFailed(f"Errore API Devices: {resp_devices.status}")
devices_json = await resp_devices.json()

# Server status
async with session.get(f"{base_url}/status") as resp_status:
if resp_status.status != 200:
raise UpdateFailed(f"Errore API Status: {resp_status.status}")
status_info = await resp_status.json()

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

return {
"zones": zones_list,
"devices": devices_list
"devices": devices_list,
"status": status_info
}

except Exception as err:
Expand Down Expand Up @@ -83,6 +91,8 @@ async def async_get_data():

# Listener per ricaricare se le opzioni cambiano
entry.async_on_unload(entry.add_update_listener(update_listener))

await async_setup_services(hass, coordinator, base_url)

return True

Expand Down Expand Up @@ -115,6 +125,7 @@ def handle_event(coordinator: DataUpdateCoordinator, event: dict):
current_data = coordinator.data
zones_list = current_data.get("zones", [])
devices_list = current_data.get("devices", [])
status_info = current_data.get("status", None)
updated = False

if event_type == "zone":
Expand Down Expand Up @@ -142,7 +153,8 @@ def handle_event(coordinator: DataUpdateCoordinator, event: dict):
if updated:
coordinator.async_set_updated_data({
"zones": zones_list,
"devices": devices_list
"devices": devices_list,
"status": status_info
})

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Expand Down
99 changes: 96 additions & 3 deletions custom_components/tado_local/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
BinarySensorEntity,
BinarySensorDeviceClass,
)
from homeassistant.const import EntityCategory
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN, MANUFACTURER, format_model
from .const import DOMAIN, MANUFACTURER, format_model, MASTER_DEVICE

_LOGGER = logging.getLogger(__name__)

Expand All @@ -25,7 +26,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e

devices = coordinator.data.get("devices", [])
for device in devices:
entities.append(TadoDeviceBattery(coordinator, device))
dev_type = device.get("device_type", "Device")
if dev_type == MASTER_DEVICE:
# Add TadoLocal status fields to MASTER_DEVICE (internet bridge) entity
entities.append(TadoBridgeConnected(coordinator, device))
entities.append(TadoCloudEnbled(coordinator, device))
entities.append(TadoCloudAuthtenticated(coordinator, device))
else:
# The internet bridge does not have a battery
entities.append(TadoDeviceBattery(coordinator, device))

async_add_entities(entities)

Expand Down Expand Up @@ -104,4 +113,88 @@ def is_on(self):
if did == self._device_id:
state = dev.get("state", dev)
return state.get("battery_low", False)
return False
return False

class TadoBridgeConnected(CoordinatorEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True
_attr_icon = "mdi:lan-connect"
_attr_translation_key = "bridge_connected"

def __init__(self, coordinator, device_data):
super().__init__(coordinator)
self._device_id = device_data.get("device_id") or device_data.get("id")
self._attr_unique_id = f"tado_local_bridge_{self._device_id}"
self._device_info_data = {
"identifiers": {(DOMAIN, "device", self._device_id)}
}

@property
def device_info(self):
return self._device_info_data

@property
def is_on(self):
status = self.coordinator.data.get("status", None)
if not status:
return False
return status.get("bridge_connected", False)

class TadoCloudEnbled(CoordinatorEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True
_attr_icon = "mdi:cloud-check-variant"
_attr_translation_key = "cloud_api_enabled"

def __init__(self, coordinator, device_data):
super().__init__(coordinator)
self._device_id = device_data.get("device_id") or device_data.get("id")
self._attr_unique_id = f"tado_local_cloud_{self._device_id}"
self._device_info_data = {
"identifiers": {(DOMAIN, "device", self._device_id)}
}

@property
def device_info(self):
return self._device_info_data

@property
def is_on(self):
status = self.coordinator.data.get("status", None)
if not status:
return False
api = status.get("cloud_api", None)
if not api:
return False
return api.get("enabled", False)

class TadoCloudAuthtenticated(CoordinatorEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True
_attr_icon = "mdi:cloud-key"
_attr_translation_key = "cloud_api_authenticated"

def __init__(self, coordinator, device_data):
super().__init__(coordinator)
self._device_id = device_data.get("device_id") or device_data.get("id")
self._attr_unique_id = f"tado_local_auth{self._device_id}"
self._device_info_data = {
"identifiers": {(DOMAIN, "device", self._device_id)}
}

@property
def device_info(self):
return self._device_info_data

@property
def is_on(self):
status = self.coordinator.data.get("status", None)
if not status:
return False
api = status.get("cloud_api", None)
if not api:
return False
return api.get("authenticated", False)
5 changes: 3 additions & 2 deletions custom_components/tado_local/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ class TadoLocalClimate(CoordinatorEntity, ClimateEntity):
def __init__(self, coordinator, initial_data, base_url):
super().__init__(coordinator)
self._zone_id = initial_data.get("zone_id") or initial_data.get("id")
self._attr_name = initial_data.get("name", f"Zona {self._zone_id}")
self._attr_name = "" # remove double name from Climate control
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

Expand All @@ -63,7 +64,7 @@ def device_info(self):
"""Device Info per la Zona Logica."""
return {
"identifiers": {(DOMAIN, "zone", self._zone_id)},
"name": self._attr_name,
"name": self._device_name,
"manufacturer": MANUFACTURER,
"model": format_model("zone_control"), # Usa "Zone Control" formattato
}
Expand Down
1 change: 1 addition & 0 deletions custom_components/tado_local/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

# Uniformiamo il produttore per far apparire il logo Tado ufficiale
MANUFACTURER = "Tado"
MASTER_DEVICE = "internet_bridge"

# Mappa per abbellire i nomi dei modelli
MODEL_MAP = {
Expand Down
10 changes: 10 additions & 0 deletions custom_components/tado_local/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"services": {
"resume_all_schedules": {
"service": "mdi:calendar-clock"
},
"turn_off_all_zones": {
"service": "mdi:radiator-off"
}
}
}
147 changes: 145 additions & 2 deletions custom_components/tado_local/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN, MANUFACTURER, format_model
from .const import DOMAIN, MANUFACTURER, format_model, MASTER_DEVICE

_LOGGER = logging.getLogger(__name__)

Expand All @@ -32,6 +32,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
devices_data = coordinator.data.get("devices", [])
for device in devices_data:
entities.append(TadoDeviceSerial(coordinator, device))
dev_type = device.get("device_type", "Device")
if dev_type == MASTER_DEVICE:
# Add TadoLocal status fields to MASTER_DEVICE (internet bridge) entity
entities.append(TadoDeviceServer(coordinator, device))
entities.append(TadoDeviceServerVersion(coordinator, device))
entities.append(TadoApiDayLimit(coordinator, device))
entities.append(TadoApiCallsLeft(coordinator, device))
entities.append(TadoApiCallsUsed(coordinator, device))

async_add_entities(entities)

Expand Down Expand Up @@ -159,4 +167,139 @@ def native_value(self):
did = dev.get("device_id") or dev.get("id")
if did == self._device_id:
return dev.get("serial_number", self._serial)
return self._serial
return self._serial

class TadoDeviceServer(CoordinatorEntity, SensorEntity):
"""Sensore seriale dispositivo."""

_attr_has_entity_name = True
_attr_translation_key = "server_status"
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_icon = "mdi:server-network"

def __init__(self, coordinator, device_data):
super().__init__(coordinator)
self._device_id = device_data.get("device_id") or device_data.get("id")
self._attr_unique_id = f"tado_local_server_{self._device_id}"

self._device_info_data = {
"identifiers": {(DOMAIN, "device", self._device_id)}
}

@property
def device_info(self):
return self._device_info_data

@property
def native_value(self):
status = self.coordinator.data.get("status", None)
if not status:
return "unknown"
return status.get("status", "unknown")

class TadoDeviceServerVersion(CoordinatorEntity, SensorEntity):
"""Sensore server dispositivo."""

_attr_has_entity_name = True
_attr_translation_key = "server_version"
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_icon = "mdi:label-outline"

def __init__(self, coordinator, device_data):
super().__init__(coordinator)
self._device_id = device_data.get("device_id") or device_data.get("id")
self._attr_unique_id = f"tado_local_server_version_{self._device_id}"

self._device_info_data = {
"identifiers": {(DOMAIN, "device", self._device_id)}
}

@property
def device_info(self):
return self._device_info_data

@property
def native_value(self):
status = self.coordinator.data.get("status", None)
if not status:
return "unknown"
return status.get("version", "unknown")

class TadoApiDayLimit(CoordinatorEntity, SensorEntity):
"""Sensore seriale dispositivo."""

_attr_has_entity_name = True
_attr_translation_key = "api_limit"
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_icon = "mdi:calendar-end-outline"
_attr_suggested_display_precision = 0

def __init__(self, coordinator, device_data):
super().__init__(coordinator)
self._device_id = device_data.get("device_id") or device_data.get("id")
self._attr_unique_id = f"tado_local_api_limit_{self._device_id}"
self._device_info_data = {
"identifiers": {(DOMAIN, "device", self._device_id)}
}

@property
def device_info(self):
return self._device_info_data

@property
def native_value(self) -> int:
rate_limit = self.coordinator.data.get("status", {}).get("cloud_api", {}).get("rate_limit", None)
return rate_limit.get("granted_calls", 0) if rate_limit else -1

class TadoApiCallsLeft(CoordinatorEntity, SensorEntity):
"""Sensore API calls dispositivo."""

_attr_has_entity_name = True
_attr_translation_key = "api_remaining"
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_icon = "mdi:counter"
_attr_suggested_display_precision = 0

def __init__(self, coordinator, device_data):
super().__init__(coordinator)
self._device_id = device_data.get("device_id") or device_data.get("id")
self._attr_unique_id = f"tado_local_api_remaining_{self._device_id}"
self._device_info_data = {
"identifiers": {(DOMAIN, "device", self._device_id)}
}

@property
def device_info(self):
return self._device_info_data

@property
def native_value(self) -> int:
rate_limit = self.coordinator.data.get("status", {}).get("cloud_api", {}).get("rate_limit", {})
return rate_limit.get("remaining_calls", 0)

class TadoApiCallsUsed(CoordinatorEntity, SensorEntity):
"""Sensore API calls dispositivo."""

_attr_has_entity_name = True
_attr_translation_key = "api_used"
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_icon = "mdi:gauge-low"
_attr_native_unit_of_measurement = PERCENTAGE
_attr_suggested_display_precision = 1

def __init__(self, coordinator, device_data):
super().__init__(coordinator)
self._device_id = device_data.get("device_id") or device_data.get("id")
self._attr_unique_id = f"tado_local_api_used_{self._device_id}"
self._device_info_data = {
"identifiers": {(DOMAIN, "device", self._device_id)}
}

@property
def device_info(self):
return self._device_info_data

@property
def native_value(self) -> float:
rate_limit = self.coordinator.data.get("status", {}).get("cloud_api", {}).get("rate_limit", {})
return rate_limit.get("usage_percent", 0)
Loading