Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
64 changes: 61 additions & 3 deletions custom_components/bosch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def _get_sensor_class_no_print(device_type, sensor_type):
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
Expand Down Expand Up @@ -159,6 +160,49 @@ def _get_sensor_class_no_print(device_type, sensor_type):
HOUR = timedelta(hours=1)


def _remove_stale_pointtapi_valve_devices(
hass: HomeAssistant, entry_id: str, uuid: str, data: dict[str, Any]
) -> None:
"""Remove obsolete thermostat-valve devices after a device refresh."""
known_types: dict[int, str] = {}
for path, resource in (data or {}).items():
if not path.startswith("/devices/device") or not path.endswith("/type"):
continue
try:
device_id = int(path.removeprefix("/devices/device").removesuffix("/type"))
except ValueError:
continue
value = resource.get("value") if isinstance(resource, dict) else None
if isinstance(value, str):
known_types[device_id] = value

if not known_types:
return

device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
prefix = f"{uuid}_trv_"
for device in list(device_registry.devices.values()):
valve_ids = [
identifier.split(prefix, 1)[1]
for domain, identifier in device.identifiers
if domain == DOMAIN and identifier.startswith(prefix)
]
if not valve_ids or any(
known_types.get(int(valve_id)) == "thermostat_valve"
for valve_id in valve_ids
if valve_id.isdigit()
):
continue
if not all(valve_id.isdigit() for valve_id in valve_ids):
continue
if any(int(valve_id) in known_types for valve_id in valve_ids):
for entity in list(entity_registry.entities.values()):
if entity.device_id == device.id and entity.config_entry_id == entry_id:
entity_registry.async_remove(entity.entity_id)
device_registry.async_remove_device(device.id)


async def async_setup_entry(hass: HomeAssistant, entry: BoschConfigEntry):
"""Create entry for Bosch thermostat device."""
uuid = entry.data[UUID]
Expand Down Expand Up @@ -381,6 +425,12 @@ async def async_init(self) -> bool:
sw_version="",
)
await coordinator.async_config_entry_first_refresh()
_remove_stale_pointtapi_valve_devices(
self.hass,
self.config_entry.entry_id,
self.uuid,
getattr(coordinator, "data", None) or {},
)
await self.hass.config_entries.async_forward_entry_setups(
self.config_entry,
[p for p in self.supported_platforms if p],
Expand Down Expand Up @@ -638,13 +688,17 @@ def rounder(t):
async_dispatcher_send(self.hass, signal)
return True

async def custom_put(self, path: str, value: Any) -> None:
async def custom_put(self, path: str, value: Any) -> Any:
"""Send PUT directly to gateway without parsing."""
await self.gateway.raw_put(path=path, value=value)
if self._protocol == POINTTAPI:
return await self.gateway.put(uri=path, value=value)
return await self.gateway.raw_put(path=path, value=value)

async def custom_get(self, path) -> str:
async def custom_get(self, path) -> Any:
"""Fetch value from gateway."""
async with self._update_lock:
if self._protocol == POINTTAPI:
return await self.gateway.get(uri=path)
return await self.gateway.raw_query(path=path)

async def component_update(self, component_type=None, event_time=None):
Expand Down Expand Up @@ -715,6 +769,10 @@ async def thermostat_refresh(self, event_time=None):
self.uuid,
event_time,
)
if self._protocol == POINTTAPI:
if coordinator := getattr(self._data, "coordinator", None):
await coordinator.async_request_refresh()
return
async with self._update_lock:
await self.component_update(SENSOR, event_time)
await self.component_update(BINARY_SENSOR, event_time)
Expand Down
1 change: 1 addition & 0 deletions custom_components/bosch/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
SERVICE_PUT_FLOAT = "send_custom_put_float"
SERVICE_GET = "send_custom_get"
SERVICE_DEBUG = "debug_scan"
SERVICE_REFRESH_GATEWAY = "refresh_gateway"
SERVICE_UPDATE = "update_thermostat"
RECORDING_SERVICE_UPDATE = "update_recordings_sensor"
SERVICE_MOVE_OLD_DATA = "move_old_statistic_data"
Expand Down
16 changes: 15 additions & 1 deletion custom_components/bosch/pointtapi_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ async def _fetch_paths(client: PoinTTAPIClient) -> dict[str, Any]:
sub = await client.get(ref_id)
if isinstance(sub, dict):
data[ref_id] = sub
# Fetch one more level for refEnum (e.g. temperatureLevels -> temperatureLevels/high)
# Fetch nested refEnum leaves such as
# device -> etrv -> childLock -> enabled.
if sub.get("type") == "refEnum":
for r2 in sub.get(REFERENCES_KEY) or []:
r2_id = r2.get(ID_KEY) if isinstance(r2, dict) else None
Expand All @@ -220,6 +221,19 @@ async def _fetch_paths(client: PoinTTAPIClient) -> dict[str, Any]:
sub2 = await client.get(r2_id)
if isinstance(sub2, dict):
data[r2_id] = sub2
if sub2.get("type") == "refEnum":
for r3 in sub2.get(REFERENCES_KEY) or []:
r3_id = r3.get(ID_KEY) if isinstance(r3, dict) else None
if not r3_id or r3_id in data:
continue
try:
leaf = await client.get(r3_id)
if isinstance(leaf, dict):
data[r3_id] = leaf
except ConfigEntryAuthFailed:
_LOGGER.debug("POINTTAPI 401/403 on ref %s, skipping", r3_id)
except Exception:
continue
except ConfigEntryAuthFailed:
_LOGGER.debug("POINTTAPI 401/403 on ref %s, skipping", r2_id)
except Exception:
Expand Down
Loading
Loading