Skip to content

Commit c9c6f33

Browse files
committed
Fix POINTTAPI switches/translations and add optional entities
1 parent 98984f3 commit c9c6f33

11 files changed

Lines changed: 371 additions & 108 deletions

File tree

custom_components/bosch/number.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
)
2121
from .pointtapi_entities import (
2222
BoschPoinTTAPINumberEntity,
23-
POINTTAPI_NUMBER_DESCRIPTIONS,
23+
_pointtapi_number_descriptions,
2424
)
2525

2626

@@ -31,11 +31,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
3131
coordinator = rt_data.coordinator
3232
if coordinator:
3333
uuid = config_entry.data.get(UUID)
34+
descriptions = _pointtapi_number_descriptions(coordinator.data or {})
3435
async_add_entities([
3536
BoschPoinTTAPINumberEntity(
3637
coordinator, config_entry.entry_id, uuid, description
3738
)
38-
for description in POINTTAPI_NUMBER_DESCRIPTIONS
39+
for description in descriptions
3940
])
4041
else:
4142
async_add_entities([])

custom_components/bosch/pointtapi_entities.py

Lines changed: 190 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@
3737
WaterHeaterEntity,
3838
WaterHeaterEntityFeature,
3939
)
40-
from homeassistant.const import UnitOfEnergy, UnitOfPressure, UnitOfTemperature, UnitOfTime
40+
from homeassistant.const import (
41+
UnitOfEnergy,
42+
UnitOfPressure,
43+
UnitOfTemperature,
44+
UnitOfTime,
45+
UnitOfVolume,
46+
)
4147
from homeassistant.util import dt as dt_util
4248
from homeassistant.core import callback
4349
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
@@ -479,6 +485,27 @@ def _thermostat_valve_field(data: dict[str, Any], valve_id: int, field: str) ->
479485
return row.get(field)
480486

481487

488+
def _thermostat_valve_battery(data: dict[str, Any], valve_id: int) -> Any:
489+
"""Return a normalized battery state for a thermostat valve."""
490+
raw = _thermostat_valve_field(data, valve_id, "battery")
491+
if isinstance(raw, str) and raw.strip().lower() == "ok":
492+
return "OK"
493+
return raw
494+
495+
496+
def _thermostat_valve_zone_name(data: dict[str, Any], valve_id: int) -> Any:
497+
"""Return a zone display name for a thermostat valve when available."""
498+
raw_zone = _thermostat_valve_field(data, valve_id, "zone")
499+
if raw_zone is None:
500+
return None
501+
502+
zone_id = f"zn{raw_zone}"
503+
zone_name = _decode_zone_name(_val(data, f"/zones/{zone_id}/name"))
504+
if isinstance(zone_name, str) and zone_name.strip():
505+
return zone_name
506+
return raw_zone
507+
508+
482509
def _thermostat_valve_device_info(
483510
uuid: str,
484511
data: dict[str, Any],
@@ -516,6 +543,7 @@ def _pointtapi_thermostat_valve_sensor_descriptions(
516543
# No SIGNAL_STRENGTH device class: HA only accepts dB/dBm for
517544
# it, and /devices/list reports link quality as a percentage.
518545
native_unit_of_measurement="%",
546+
icon="mdi:signal",
519547
entity_category=EntityCategory.DIAGNOSTIC,
520548
value_fn=lambda d, vid=valve_id: _thermostat_valve_field(d, vid, "signal"),
521549
available_fn=lambda d, vid=valve_id: _thermostat_valve_row_by_id(d, vid) is not None,
@@ -524,16 +552,17 @@ def _pointtapi_thermostat_valve_sensor_descriptions(
524552
BoschPoinTTAPISensorEntityDescription(
525553
key=f"/devices/list/thermostat_valve/{valve_id}/battery",
526554
translation_key="thermostat_valve_battery",
555+
icon="mdi:battery",
527556
entity_category=EntityCategory.DIAGNOSTIC,
528-
value_fn=lambda d, vid=valve_id: _thermostat_valve_field(d, vid, "battery"),
557+
value_fn=lambda d, vid=valve_id: _thermostat_valve_battery(d, vid),
529558
available_fn=lambda d, vid=valve_id: _thermostat_valve_row_by_id(d, vid) is not None,
530559
device_info_fn=lambda u, d, lang=None, vid=valve_id: _thermostat_valve_device_info(u, d, vid, lang),
531560
),
532561
BoschPoinTTAPISensorEntityDescription(
533562
key=f"/devices/list/thermostat_valve/{valve_id}/zone",
534563
translation_key="thermostat_valve_zone",
535564
entity_category=EntityCategory.DIAGNOSTIC,
536-
value_fn=lambda d, vid=valve_id: _thermostat_valve_field(d, vid, "zone"),
565+
value_fn=lambda d, vid=valve_id: _thermostat_valve_zone_name(d, vid),
537566
available_fn=lambda d, vid=valve_id: _thermostat_valve_row_by_id(d, vid) is not None,
538567
device_info_fn=lambda u, d, lang=None, vid=valve_id: _thermostat_valve_device_info(u, d, vid, lang),
539568
),
@@ -637,6 +666,12 @@ def _start_of_today() -> Any:
637666
return dt_util.start_of_local_day()
638667

639668

669+
def _start_of_month() -> Any:
670+
"""Return start of the current month in local timezone for last_reset."""
671+
now = dt_util.now()
672+
return now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
673+
674+
640675
# ── Hourly gas usage helper functions ────────────────────────────────────────
641676

642677

@@ -1089,17 +1124,30 @@ def _pointtapi_electricity_average_sensor_descriptions(
10891124
return ()
10901125

10911126
candidates = (
1092-
("/energy/electricity/dayAverage", "electricity_day_average"),
1093-
("/energy/electricity/monthAverage", "electricity_month_average"),
1127+
(
1128+
"/energy/electricity/dayAverage",
1129+
"electricity_day_average",
1130+
_start_of_today,
1131+
),
1132+
(
1133+
"/energy/electricity/monthAverage",
1134+
"electricity_month_average",
1135+
_start_of_month,
1136+
),
10941137
)
10951138

10961139
descriptions: list[BoschPoinTTAPISensorEntityDescription] = []
1097-
for path, translation_key in candidates:
1140+
for path, translation_key, last_reset_fn in candidates:
10981141
if isinstance(data.get(path), dict) and _path_available(data, path):
10991142
descriptions.append(
11001143
BoschPoinTTAPISensorEntityDescription(
11011144
key=path,
11021145
translation_key=translation_key,
1146+
device_class=SensorDeviceClass.ENERGY,
1147+
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
1148+
state_class=SensorStateClass.TOTAL,
1149+
value_fn=lambda d, p=path: _val(d, p),
1150+
last_reset_fn=last_reset_fn,
11031151
)
11041152
)
11051153
return tuple(descriptions)
@@ -1410,6 +1458,15 @@ def _pointtapi_sensor_descriptions(
14101458
),
14111459
]
14121460

1461+
if isinstance(data.get("/gateway/zigbee/versionFirmware"), dict):
1462+
descriptions.append(
1463+
BoschPoinTTAPISensorEntityDescription(
1464+
key="/gateway/zigbee/versionFirmware",
1465+
translation_key="zigbee_firmware_version",
1466+
entity_category=EntityCategory.DIAGNOSTIC,
1467+
)
1468+
)
1469+
14131470
if _gateway_ui_has_eco_reference(data):
14141471
descriptions.append(
14151472
BoschPoinTTAPISensorEntityDescription(
@@ -1510,105 +1567,131 @@ def last_reset(self) -> Any:
15101567
# ── Number entities (boost settings) ─────────────────────────────────────────
15111568

15121569

1513-
POINTTAPI_NUMBER_DESCRIPTIONS: tuple[NumberEntityDescription, ...] = (
1514-
NumberEntityDescription(
1515-
key="/heatingCircuits/hc1/boostTemperature",
1516-
translation_key="boost_temperature",
1517-
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1518-
native_min_value=5.0,
1519-
native_max_value=30.0,
1520-
native_step=0.5,
1521-
),
1522-
NumberEntityDescription(
1523-
key="/heatingCircuits/hc1/boostDuration",
1524-
translation_key="boost_duration",
1525-
native_unit_of_measurement=UnitOfTime.HOURS,
1526-
native_min_value=0.5,
1527-
native_max_value=24.0,
1528-
native_step=0.5,
1529-
),
1530-
# ── Heating circuit configuration (2b) ───────────────────────────────────
1531-
NumberEntityDescription(
1532-
key="/heatingCircuits/hc1/maxSupply",
1533-
translation_key="max_supply_temperature",
1534-
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1535-
native_min_value=25.0,
1536-
native_max_value=90.0,
1537-
native_step=1.0,
1538-
entity_category=EntityCategory.CONFIG,
1539-
),
1540-
NumberEntityDescription(
1541-
key="/heatingCircuits/hc1/minSupply",
1542-
translation_key="min_supply_temperature",
1543-
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1544-
native_min_value=10.0,
1545-
native_max_value=90.0,
1546-
native_step=1.0,
1547-
entity_category=EntityCategory.CONFIG,
1548-
),
1549-
NumberEntityDescription(
1550-
key="/heatingCircuits/hc1/nightThreshold",
1551-
translation_key="night_setback_threshold",
1552-
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1553-
native_min_value=5.0,
1554-
native_max_value=30.0,
1555-
native_step=0.5,
1556-
entity_category=EntityCategory.CONFIG,
1557-
),
1558-
NumberEntityDescription(
1559-
key="/heatingCircuits/hc1/suWiThreshold",
1560-
translation_key="summer_winter_threshold",
1561-
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1562-
native_min_value=10.0,
1563-
native_max_value=30.0,
1564-
native_step=0.5,
1565-
entity_category=EntityCategory.CONFIG,
1566-
),
1567-
NumberEntityDescription(
1568-
key="/heatingCircuits/hc1/roomInfluence",
1569-
translation_key="room_influence",
1570-
native_min_value=0.0,
1571-
native_max_value=3.0,
1572-
native_step=1.0,
1573-
entity_category=EntityCategory.CONFIG,
1574-
),
1575-
NumberEntityDescription(
1576-
key="/system/sensors/temperatures/offset",
1577-
translation_key="temperature_calibration_offset",
1578-
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1579-
native_min_value=-5.0,
1580-
native_max_value=5.0,
1581-
native_step=0.5,
1582-
entity_category=EntityCategory.CONFIG,
1583-
),
1584-
NumberEntityDescription(
1585-
key="/energy/gas/annualGoal",
1586-
translation_key="annual_gas_goal",
1587-
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
1588-
native_min_value=0.0,
1589-
native_max_value=1000000.0,
1590-
native_step=1.0,
1591-
entity_category=EntityCategory.CONFIG,
1592-
),
1593-
# ── v1.0.0 comfort controls (constraints from boost-probe-notes.md) ───────
1594-
NumberEntityDescription(
1595-
key="/dhwCircuits/dhw1/extraDhwDuration",
1596-
translation_key="extra_hot_water_duration",
1597-
native_unit_of_measurement=UnitOfTime.MINUTES,
1598-
native_min_value=15.0,
1599-
native_max_value=2880.0,
1600-
native_step=15.0,
1601-
),
1602-
NumberEntityDescription(
1603-
key="/dhwCircuits/dhw1/thermalDisinfect/time",
1604-
translation_key="thermal_disinfect_time",
1605-
native_unit_of_measurement=UnitOfTime.MINUTES,
1606-
native_min_value=0.0,
1607-
native_max_value=1439.0,
1608-
native_step=1.0,
1609-
entity_category=EntityCategory.CONFIG,
1610-
),
1611-
)
1570+
def _pointtapi_number_descriptions(
1571+
data: dict[str, Any] | None = None,
1572+
) -> tuple[NumberEntityDescription, ...]:
1573+
"""Return POINTTAPI number descriptions, plus optional yearly energy goals."""
1574+
descriptions: list[NumberEntityDescription] = [
1575+
NumberEntityDescription(
1576+
key="/heatingCircuits/hc1/boostTemperature",
1577+
translation_key="boost_temperature",
1578+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1579+
native_min_value=5.0,
1580+
native_max_value=30.0,
1581+
native_step=0.5,
1582+
),
1583+
NumberEntityDescription(
1584+
key="/heatingCircuits/hc1/boostDuration",
1585+
translation_key="boost_duration",
1586+
native_unit_of_measurement=UnitOfTime.HOURS,
1587+
native_min_value=0.5,
1588+
native_max_value=24.0,
1589+
native_step=0.5,
1590+
),
1591+
# ── Heating circuit configuration (2b) ───────────────────────────────────
1592+
NumberEntityDescription(
1593+
key="/heatingCircuits/hc1/maxSupply",
1594+
translation_key="max_supply_temperature",
1595+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1596+
native_min_value=25.0,
1597+
native_max_value=90.0,
1598+
native_step=1.0,
1599+
entity_category=EntityCategory.CONFIG,
1600+
),
1601+
NumberEntityDescription(
1602+
key="/heatingCircuits/hc1/minSupply",
1603+
translation_key="min_supply_temperature",
1604+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1605+
native_min_value=10.0,
1606+
native_max_value=90.0,
1607+
native_step=1.0,
1608+
entity_category=EntityCategory.CONFIG,
1609+
),
1610+
NumberEntityDescription(
1611+
key="/heatingCircuits/hc1/nightThreshold",
1612+
translation_key="night_setback_threshold",
1613+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1614+
native_min_value=5.0,
1615+
native_max_value=30.0,
1616+
native_step=0.5,
1617+
entity_category=EntityCategory.CONFIG,
1618+
),
1619+
NumberEntityDescription(
1620+
key="/heatingCircuits/hc1/suWiThreshold",
1621+
translation_key="summer_winter_threshold",
1622+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1623+
native_min_value=10.0,
1624+
native_max_value=30.0,
1625+
native_step=0.5,
1626+
entity_category=EntityCategory.CONFIG,
1627+
),
1628+
NumberEntityDescription(
1629+
key="/heatingCircuits/hc1/roomInfluence",
1630+
translation_key="room_influence",
1631+
native_min_value=0.0,
1632+
native_max_value=3.0,
1633+
native_step=1.0,
1634+
entity_category=EntityCategory.CONFIG,
1635+
),
1636+
NumberEntityDescription(
1637+
key="/system/sensors/temperatures/offset",
1638+
translation_key="temperature_calibration_offset",
1639+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
1640+
native_min_value=-5.0,
1641+
native_max_value=5.0,
1642+
native_step=0.5,
1643+
entity_category=EntityCategory.CONFIG,
1644+
),
1645+
# ── v1.0.0 comfort controls (constraints from boost-probe-notes.md) ───────
1646+
NumberEntityDescription(
1647+
key="/dhwCircuits/dhw1/extraDhwDuration",
1648+
translation_key="extra_hot_water_duration",
1649+
native_unit_of_measurement=UnitOfTime.MINUTES,
1650+
native_min_value=15.0,
1651+
native_max_value=2880.0,
1652+
native_step=15.0,
1653+
),
1654+
NumberEntityDescription(
1655+
key="/dhwCircuits/dhw1/thermalDisinfect/time",
1656+
translation_key="thermal_disinfect_time",
1657+
native_unit_of_measurement=UnitOfTime.MINUTES,
1658+
native_min_value=0.0,
1659+
native_max_value=1439.0,
1660+
native_step=1.0,
1661+
entity_category=EntityCategory.CONFIG,
1662+
),
1663+
]
1664+
1665+
if isinstance((data or {}).get("/energy/gas/annualGoal"), dict):
1666+
descriptions.append(
1667+
NumberEntityDescription(
1668+
key="/energy/gas/annualGoal",
1669+
translation_key="annual_gas_goal",
1670+
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
1671+
native_min_value=0.0,
1672+
native_max_value=1000000.0,
1673+
native_step=1.0,
1674+
entity_category=EntityCategory.CONFIG,
1675+
)
1676+
)
1677+
1678+
if isinstance((data or {}).get("/energy/electricity/annualGoal"), dict):
1679+
descriptions.append(
1680+
NumberEntityDescription(
1681+
key="/energy/electricity/annualGoal",
1682+
translation_key="annual_electricity_goal",
1683+
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
1684+
native_min_value=0.0,
1685+
native_max_value=1000000.0,
1686+
native_step=1.0,
1687+
entity_category=EntityCategory.CONFIG,
1688+
)
1689+
)
1690+
1691+
return tuple(descriptions)
1692+
1693+
1694+
POINTTAPI_NUMBER_DESCRIPTIONS: tuple[NumberEntityDescription, ...] = _pointtapi_number_descriptions()
16121695

16131696

16141697
class BoschPoinTTAPINumberEntity(
@@ -2073,6 +2156,8 @@ class BoschPoinTTAPISwitchEntityDescription(SwitchEntityDescription):
20732156
BoschPoinTTAPISwitchEntityDescription(
20742157
key="/dhwCircuits/dhw1/thermalDisinfect/state",
20752158
translation_key="thermal_disinfect",
2159+
on_value="on",
2160+
off_value="off",
20762161
device_id_suffix="dhw1",
20772162
device_name_override="Water heater",
20782163
),

0 commit comments

Comments
 (0)