|
@@ -2,21 +2,46 @@
|
|
| 2 |
|
| 3 |
from typing import Any
|
| 4 |
|
| 5 |
-
from homeassistant.const import Platform
|
| 6 |
from homeassistant.core import HomeAssistant, callback
|
| 7 |
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
|
|
| 8 |
|
| 9 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
| 14 |
-
"""Set up Plugwise
|
| 15 |
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
await coordinator.async_config_entry_first_refresh()
|
| 19 |
-
|
|
|
|
| 20 |
|
| 21 |
entry.runtime_data = coordinator
|
| 22 |
|
|
@@ -33,20 +58,25 @@
|
|
| 33 |
|
| 34 |
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
| 35 |
|
|
|
|
|
|
|
| 36 |
return True
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
| 40 |
-
"""Unload
|
| 41 |
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
| 42 |
|
| 43 |
-
|
| 44 |
@callback
|
| 45 |
def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
|
| 46 |
"""Migrate Plugwise entity entries.
|
| 47 |
|
| 48 |
-
Migrates old unique ID's from old binary_sensors and
|
| 49 |
-
switches to the new unique ID's.
|
| 50 |
"""
|
| 51 |
if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
|
| 52 |
"-slave_boiler_state"
|
|
@@ -68,18 +98,17 @@
|
|
| 68 |
# No migration needed
|
| 69 |
return None
|
| 70 |
|
| 71 |
-
|
| 72 |
-
def migrate_sensor_entities(
|
| 73 |
hass: HomeAssistant,
|
| 74 |
coordinator: PlugwiseDataUpdateCoordinator,
|
| 75 |
) -> None:
|
| 76 |
"""Migrate Sensors if needed."""
|
| 77 |
ent_reg = er.async_get(hass)
|
| 78 |
|
| 79 |
-
#
|
| 80 |
# to opentherm_outdoor_air_temperature sensor
|
| 81 |
for device_id, device in coordinator.data.items():
|
| 82 |
-
if device[
|
| 83 |
continue
|
| 84 |
|
| 85 |
old_unique_id = f"{device_id}-outdoor_temperature"
|
|
@@ -87,10 +116,27 @@
|
|
| 87 |
Platform.SENSOR, DOMAIN, old_unique_id
|
| 88 |
):
|
| 89 |
new_unique_id = f"{device_id}-outdoor_air_temperature"
|
| 90 |
-
LOGGER
|
| 91 |
-
"Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
|
| 92 |
-
entity_id,
|
| 93 |
-
old_unique_id,
|
| 94 |
-
new_unique_id,
|
| 95 |
-
)
|
| 96 |
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from typing import Any
|
| 4 |
|
| 5 |
+
from homeassistant.const import CONF_TIMEOUT, Platform
|
| 6 |
from homeassistant.core import HomeAssistant, callback
|
| 7 |
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
| 8 |
+
from homeassistant.helpers.typing import ConfigType
|
| 9 |
|
| 10 |
+
from .const import (
|
| 11 |
+
CONF_REFRESH_INTERVAL, # pw-beta options
|
| 12 |
+
DOMAIN,
|
| 13 |
+
LOGGER,
|
| 14 |
+
PLATFORMS,
|
| 15 |
+
)
|
| 16 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 17 |
+
from .services import async_setup_services
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
async def async_setup(hass: HomeAssistant, _config: ConfigType) -> bool:
|
| 21 |
+
"""Set up my integration."""
|
| 22 |
+
|
| 23 |
+
async_setup_services(hass)
|
| 24 |
+
|
| 25 |
+
return True
|
| 26 |
|
| 27 |
|
| 28 |
async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
| 29 |
+
"""Set up Plugwise from a config entry."""
|
| 30 |
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
| 31 |
|
| 32 |
+
cooldown = 1.5 # pw-beta frontend refresh-interval
|
| 33 |
+
if (
|
| 34 |
+
custom_refresh := entry.options.get(CONF_REFRESH_INTERVAL)
|
| 35 |
+
) is not None: # pragma: no cover
|
| 36 |
+
cooldown = custom_refresh
|
| 37 |
+
LOGGER.debug("DUC cooldown interval: %s", cooldown)
|
| 38 |
+
|
| 39 |
+
coordinator = PlugwiseDataUpdateCoordinator(
|
| 40 |
+
hass, cooldown, entry
|
| 41 |
+
) # pw-beta - cooldown, update_interval as extra
|
| 42 |
await coordinator.async_config_entry_first_refresh()
|
| 43 |
+
|
| 44 |
+
await async_migrate_sensor_entities(hass, coordinator)
|
| 45 |
|
| 46 |
entry.runtime_data = coordinator
|
| 47 |
|
|
|
|
| 58 |
|
| 59 |
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
| 60 |
|
| 61 |
+
entry.async_on_unload(entry.add_update_listener(update_listener)) # pw-beta options_flow
|
| 62 |
+
|
| 63 |
return True
|
| 64 |
|
| 65 |
+
async def update_listener(
|
| 66 |
+
hass: HomeAssistant, entry: PlugwiseConfigEntry
|
| 67 |
+
) -> None: # pragma: no cover # pw-beta
|
| 68 |
+
"""Handle options update."""
|
| 69 |
+
await hass.config_entries.async_reload(entry.entry_id)
|
| 70 |
|
| 71 |
async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
| 72 |
+
"""Unload Plugwise."""
|
| 73 |
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
| 74 |
|
|
|
|
| 75 |
@callback
|
| 76 |
def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
|
| 77 |
"""Migrate Plugwise entity entries.
|
| 78 |
|
| 79 |
+
- Migrates old unique ID's from old binary_sensors and switches to the new unique ID's
|
|
|
|
| 80 |
"""
|
| 81 |
if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
|
| 82 |
"-slave_boiler_state"
|
|
|
|
| 98 |
# No migration needed
|
| 99 |
return None
|
| 100 |
|
| 101 |
+
async def async_migrate_sensor_entities(
|
|
|
|
| 102 |
hass: HomeAssistant,
|
| 103 |
coordinator: PlugwiseDataUpdateCoordinator,
|
| 104 |
) -> None:
|
| 105 |
"""Migrate Sensors if needed."""
|
| 106 |
ent_reg = er.async_get(hass)
|
| 107 |
|
| 108 |
+
# Migrate opentherm_outdoor_temperature
|
| 109 |
# to opentherm_outdoor_air_temperature sensor
|
| 110 |
for device_id, device in coordinator.data.items():
|
| 111 |
+
if device["dev_class"] != "heater_central":
|
| 112 |
continue
|
| 113 |
|
| 114 |
old_unique_id = f"{device_id}-outdoor_temperature"
|
|
|
|
| 116 |
Platform.SENSOR, DOMAIN, old_unique_id
|
| 117 |
):
|
| 118 |
new_unique_id = f"{device_id}-outdoor_air_temperature"
|
| 119 |
+
# Upstream remove LOGGER debug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
| 121 |
+
|
| 122 |
+
# pw-beta only - revert adding CONF_TIMEOUT to config_entry in v0.53.3
|
| 123 |
+
async def async_migrate_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
| 124 |
+
"""Migrate back to v1.1 config entry."""
|
| 125 |
+
if entry.version > 1:
|
| 126 |
+
# This means the user has downgraded from a future version
|
| 127 |
+
return False #pragma: no cover
|
| 128 |
+
|
| 129 |
+
if entry.version == 1 and entry.minor_version == 2:
|
| 130 |
+
new_data = {**entry.data}
|
| 131 |
+
new_data.pop(CONF_TIMEOUT)
|
| 132 |
+
hass.config_entries.async_update_entry(
|
| 133 |
+
entry, data=new_data, minor_version=1, version=1
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
LOGGER.debug(
|
| 137 |
+
"Migration to version %s.%s successful",
|
| 138 |
+
entry.version,
|
| 139 |
+
entry.minor_version,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
return True
|
|
@@ -2,10 +2,13 @@
|
|
| 2 |
|
| 3 |
from collections.abc import Mapping
|
| 4 |
from dataclasses import dataclass
|
| 5 |
-
from typing import Any
|
| 6 |
|
| 7 |
from plugwise.constants import BinarySensorType
|
| 8 |
|
|
|
|
|
|
|
|
|
|
| 9 |
from homeassistant.components.binary_sensor import (
|
| 10 |
BinarySensorDeviceClass,
|
| 11 |
BinarySensorEntity,
|
|
@@ -15,11 +18,26 @@
|
|
| 15 |
from homeassistant.core import HomeAssistant, callback
|
| 16 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 19 |
from .entity import PlugwiseEntity
|
| 20 |
|
| 21 |
-
SEVERITIES = ["other", "info", "warning", "error"]
|
| 22 |
-
|
| 23 |
# Coordinator is used to centralize the data updates
|
| 24 |
PARALLEL_UPDATES = 0
|
| 25 |
|
|
@@ -31,50 +49,51 @@
|
|
| 31 |
key: BinarySensorType
|
| 32 |
|
| 33 |
|
| 34 |
-
|
|
|
|
| 35 |
PlugwiseBinarySensorEntityDescription(
|
| 36 |
-
key=
|
| 37 |
device_class=BinarySensorDeviceClass.BATTERY,
|
| 38 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 39 |
),
|
| 40 |
PlugwiseBinarySensorEntityDescription(
|
| 41 |
-
key=
|
| 42 |
-
translation_key=
|
| 43 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 44 |
),
|
| 45 |
PlugwiseBinarySensorEntityDescription(
|
| 46 |
-
key=
|
| 47 |
-
translation_key=
|
| 48 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 49 |
),
|
| 50 |
PlugwiseBinarySensorEntityDescription(
|
| 51 |
-
key=
|
| 52 |
-
translation_key=
|
| 53 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 54 |
),
|
| 55 |
PlugwiseBinarySensorEntityDescription(
|
| 56 |
-
key=
|
| 57 |
-
translation_key=
|
| 58 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 59 |
),
|
| 60 |
PlugwiseBinarySensorEntityDescription(
|
| 61 |
-
key=
|
| 62 |
-
translation_key=
|
| 63 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 64 |
),
|
| 65 |
PlugwiseBinarySensorEntityDescription(
|
| 66 |
-
key=
|
| 67 |
-
translation_key=
|
| 68 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 69 |
),
|
| 70 |
PlugwiseBinarySensorEntityDescription(
|
| 71 |
-
key=
|
| 72 |
-
translation_key=
|
| 73 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 74 |
),
|
| 75 |
PlugwiseBinarySensorEntityDescription(
|
| 76 |
-
key=
|
| 77 |
-
translation_key=
|
| 78 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 79 |
),
|
| 80 |
)
|
|
@@ -85,7 +104,7 @@
|
|
| 85 |
entry: PlugwiseConfigEntry,
|
| 86 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 87 |
) -> None:
|
| 88 |
-
"""Set up
|
| 89 |
coordinator = entry.runtime_data
|
| 90 |
|
| 91 |
@callback
|
|
@@ -94,20 +113,40 @@
|
|
| 94 |
if not coordinator.new_devices:
|
| 95 |
return
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
_add_entities()
|
| 106 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 107 |
|
| 108 |
|
| 109 |
class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
|
| 110 |
-
"""
|
| 111 |
|
| 112 |
entity_description: PlugwiseBinarySensorEntityDescription
|
| 113 |
|
|
@@ -121,28 +160,46 @@
|
|
| 121 |
super().__init__(coordinator, device_id)
|
| 122 |
self.entity_description = description
|
| 123 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
|
|
|
| 124 |
|
| 125 |
@property
|
| 126 |
-
|
| 127 |
-
def is_on(self) -> bool:
|
| 128 |
"""Return true if the binary sensor is on."""
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
@property
|
| 132 |
-
@override
|
| 133 |
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
| 134 |
"""Return entity specific state attributes."""
|
| 135 |
-
if self.entity_description.key !=
|
| 136 |
return None
|
| 137 |
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
gateway_id = self.coordinator.api.gateway_id
|
| 140 |
if notify := self.coordinator.data[gateway_id]["notifications"]:
|
| 141 |
-
for details in notify.
|
| 142 |
for msg_type, msg in details.items():
|
| 143 |
msg_type = msg_type.lower()
|
| 144 |
if msg_type not in SEVERITIES:
|
| 145 |
-
msg_type = "other"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
attrs[f"{msg_type}_msg"].append(msg)
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
return attrs
|
|
|
|
| 2 |
|
| 3 |
from collections.abc import Mapping
|
| 4 |
from dataclasses import dataclass
|
| 5 |
+
from typing import Any
|
| 6 |
|
| 7 |
from plugwise.constants import BinarySensorType
|
| 8 |
|
| 9 |
+
from homeassistant.components import (
|
| 10 |
+
persistent_notification, # pw-beta Plugwise notifications
|
| 11 |
+
)
|
| 12 |
from homeassistant.components.binary_sensor import (
|
| 13 |
BinarySensorDeviceClass,
|
| 14 |
BinarySensorEntity,
|
|
|
|
| 18 |
from homeassistant.core import HomeAssistant, callback
|
| 19 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 20 |
|
| 21 |
+
from .const import (
|
| 22 |
+
BATTERY_STATE,
|
| 23 |
+
BINARY_SENSORS,
|
| 24 |
+
COMPRESSOR_STATE,
|
| 25 |
+
COOLING_ENABLED,
|
| 26 |
+
COOLING_STATE,
|
| 27 |
+
DHW_STATE,
|
| 28 |
+
DOMAIN,
|
| 29 |
+
FLAME_STATE,
|
| 30 |
+
HEATING_STATE,
|
| 31 |
+
LOGGER, # pw-beta
|
| 32 |
+
PLUGWISE_NOTIFICATION,
|
| 33 |
+
SECONDARY_BOILER_STATE,
|
| 34 |
+
SEVERITIES,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# Upstream
|
| 38 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 39 |
from .entity import PlugwiseEntity
|
| 40 |
|
|
|
|
|
|
|
| 41 |
# Coordinator is used to centralize the data updates
|
| 42 |
PARALLEL_UPDATES = 0
|
| 43 |
|
|
|
|
| 49 |
key: BinarySensorType
|
| 50 |
|
| 51 |
|
| 52 |
+
# Upstream PLUGWISE_BINARY_SENSORS
|
| 53 |
+
PLUGWISE_BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
|
| 54 |
PlugwiseBinarySensorEntityDescription(
|
| 55 |
+
key=BATTERY_STATE,
|
| 56 |
device_class=BinarySensorDeviceClass.BATTERY,
|
| 57 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 58 |
),
|
| 59 |
PlugwiseBinarySensorEntityDescription(
|
| 60 |
+
key=COMPRESSOR_STATE,
|
| 61 |
+
translation_key=COMPRESSOR_STATE,
|
| 62 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 63 |
),
|
| 64 |
PlugwiseBinarySensorEntityDescription(
|
| 65 |
+
key=COOLING_ENABLED,
|
| 66 |
+
translation_key=COOLING_ENABLED,
|
| 67 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 68 |
),
|
| 69 |
PlugwiseBinarySensorEntityDescription(
|
| 70 |
+
key=DHW_STATE,
|
| 71 |
+
translation_key=DHW_STATE,
|
| 72 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 73 |
),
|
| 74 |
PlugwiseBinarySensorEntityDescription(
|
| 75 |
+
key=FLAME_STATE,
|
| 76 |
+
translation_key=FLAME_STATE,
|
| 77 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 78 |
),
|
| 79 |
PlugwiseBinarySensorEntityDescription(
|
| 80 |
+
key=HEATING_STATE,
|
| 81 |
+
translation_key=HEATING_STATE,
|
| 82 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 83 |
),
|
| 84 |
PlugwiseBinarySensorEntityDescription(
|
| 85 |
+
key=COOLING_STATE,
|
| 86 |
+
translation_key=COOLING_STATE,
|
| 87 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 88 |
),
|
| 89 |
PlugwiseBinarySensorEntityDescription(
|
| 90 |
+
key=SECONDARY_BOILER_STATE,
|
| 91 |
+
translation_key=SECONDARY_BOILER_STATE,
|
| 92 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 93 |
),
|
| 94 |
PlugwiseBinarySensorEntityDescription(
|
| 95 |
+
key=PLUGWISE_NOTIFICATION,
|
| 96 |
+
translation_key=PLUGWISE_NOTIFICATION,
|
| 97 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 98 |
),
|
| 99 |
)
|
|
|
|
| 104 |
entry: PlugwiseConfigEntry,
|
| 105 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 106 |
) -> None:
|
| 107 |
+
"""Set up Plugwise binary_sensors from a config entry."""
|
| 108 |
coordinator = entry.runtime_data
|
| 109 |
|
| 110 |
@callback
|
|
|
|
| 113 |
if not coordinator.new_devices:
|
| 114 |
return
|
| 115 |
|
| 116 |
+
# Upstream consts to HA
|
| 117 |
+
# async_add_entities(
|
| 118 |
+
# PlugwiseBinarySensorEntity(coordinator, device_id, description)
|
| 119 |
+
# for device_id in coordinator.new_devices
|
| 120 |
+
# if (
|
| 121 |
+
# binary_sensors := coordinator.data.devices[device_id].get(
|
| 122 |
+
# BINARY_SENSORS
|
| 123 |
+
# )
|
| 124 |
+
# )
|
| 125 |
+
# for description in PLUGWISE_BINARY_SENSORS
|
| 126 |
+
# if description.key in binary_sensors
|
| 127 |
+
# )
|
| 128 |
+
|
| 129 |
+
# pw-beta alternative for debugging
|
| 130 |
+
entities: list[PlugwiseBinarySensorEntity] = []
|
| 131 |
+
for device_id in coordinator.new_devices:
|
| 132 |
+
device = coordinator.data[device_id]
|
| 133 |
+
if not (binary_sensors := device.get(BINARY_SENSORS)):
|
| 134 |
+
continue
|
| 135 |
+
for description in PLUGWISE_BINARY_SENSORS:
|
| 136 |
+
if description.key not in binary_sensors:
|
| 137 |
+
continue
|
| 138 |
+
entities.append(PlugwiseBinarySensorEntity(coordinator, device_id, description))
|
| 139 |
+
LOGGER.debug(
|
| 140 |
+
"Add %s %s binary sensor", device["name"], description.translation_key or description.key
|
| 141 |
+
)
|
| 142 |
+
async_add_entities(entities)
|
| 143 |
|
| 144 |
_add_entities()
|
| 145 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 146 |
|
| 147 |
|
| 148 |
class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
|
| 149 |
+
"""Set up Plugwise binary_sensors from a config entry."""
|
| 150 |
|
| 151 |
entity_description: PlugwiseBinarySensorEntityDescription
|
| 152 |
|
|
|
|
| 160 |
super().__init__(coordinator, device_id)
|
| 161 |
self.entity_description = description
|
| 162 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 163 |
+
self._notification: dict[str, str] = {} # pw-beta
|
| 164 |
|
| 165 |
@property
|
| 166 |
+
def is_on(self) -> bool | None:
|
|
|
|
| 167 |
"""Return true if the binary sensor is on."""
|
| 168 |
+
# pw-beta: show Plugwise notifications as HA persistent notifications
|
| 169 |
+
if self._notification:
|
| 170 |
+
for notify_id, message in self._notification.items():
|
| 171 |
+
persistent_notification.async_create(
|
| 172 |
+
self.hass, message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
return self.device.get(BINARY_SENSORS, {}).get(self.entity_description.key)
|
| 176 |
|
| 177 |
@property
|
|
|
|
| 178 |
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
| 179 |
"""Return entity specific state attributes."""
|
| 180 |
+
if self.entity_description.key != PLUGWISE_NOTIFICATION: # Upstream const
|
| 181 |
return None
|
| 182 |
|
| 183 |
+
# pw-beta adjustment with attrs is to only represent severities *with* content
|
| 184 |
+
# not all severities including those without content as empty lists
|
| 185 |
+
attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
|
| 186 |
+
self._notification = {} # pw-beta
|
| 187 |
gateway_id = self.coordinator.api.gateway_id
|
| 188 |
if notify := self.coordinator.data[gateway_id]["notifications"]:
|
| 189 |
+
for notify_id, details in notify.items(): # pw-beta uses notify_id
|
| 190 |
for msg_type, msg in details.items():
|
| 191 |
msg_type = msg_type.lower()
|
| 192 |
if msg_type not in SEVERITIES:
|
| 193 |
+
msg_type = "other" # pragma: no cover
|
| 194 |
+
|
| 195 |
+
if (
|
| 196 |
+
f"{msg_type}_msg" not in attrs
|
| 197 |
+
): # pw-beta Re-evaluate against Core
|
| 198 |
+
attrs[f"{msg_type}_msg"] = []
|
| 199 |
attrs[f"{msg_type}_msg"].append(msg)
|
| 200 |
|
| 201 |
+
self._notification[
|
| 202 |
+
notify_id
|
| 203 |
+
] = f"{msg_type.title()}: {msg}" # pw-beta
|
| 204 |
+
|
| 205 |
return attrs
|
|
@@ -1,13 +1,14 @@
|
|
| 1 |
"""Plugwise Button component for Home Assistant."""
|
| 2 |
|
| 3 |
-
from typing import override
|
| 4 |
-
|
| 5 |
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
|
| 6 |
from homeassistant.const import EntityCategory
|
| 7 |
from homeassistant.core import HomeAssistant
|
| 8 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 9 |
|
| 10 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
| 11 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 12 |
from .entity import PlugwiseEntity
|
| 13 |
from .util import plugwise_command
|
|
@@ -20,14 +21,21 @@
|
|
| 20 |
entry: PlugwiseConfigEntry,
|
| 21 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 22 |
) -> None:
|
| 23 |
-
"""Set up
|
| 24 |
coordinator = entry.runtime_data
|
| 25 |
|
| 26 |
-
async_add_entities(
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
|
|
@@ -47,7 +55,6 @@
|
|
| 47 |
self._attr_unique_id = f"{device_id}-reboot"
|
| 48 |
|
| 49 |
@plugwise_command
|
| 50 |
-
@override
|
| 51 |
async def async_press(self) -> None:
|
| 52 |
"""Triggers the Plugwise button press service."""
|
| 53 |
await self.coordinator.api.reboot_gateway()
|
|
|
|
| 1 |
"""Plugwise Button component for Home Assistant."""
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
|
| 4 |
from homeassistant.const import EntityCategory
|
| 5 |
from homeassistant.core import HomeAssistant
|
| 6 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 7 |
|
| 8 |
+
from .const import (
|
| 9 |
+
LOGGER, # pw-betea
|
| 10 |
+
REBOOT,
|
| 11 |
+
)
|
| 12 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 13 |
from .entity import PlugwiseEntity
|
| 14 |
from .util import plugwise_command
|
|
|
|
| 21 |
entry: PlugwiseConfigEntry,
|
| 22 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 23 |
) -> None:
|
| 24 |
+
"""Set up Plugwise buttons from a config entry."""
|
| 25 |
coordinator = entry.runtime_data
|
| 26 |
|
| 27 |
+
# async_add_entities(
|
| 28 |
+
# PlugwiseButtonEntity(coordinator, device_id)
|
| 29 |
+
# for device_id in coordinator.data.devices
|
| 30 |
+
# if device_id == gateway[GATEWAY_ID] and REBOOT in gateway
|
| 31 |
+
# )
|
| 32 |
+
# pw-beta alternative for debugging
|
| 33 |
+
entities: list[PlugwiseButtonEntity] = []
|
| 34 |
+
for device_id, device in coordinator.data.items():
|
| 35 |
+
if device_id == coordinator.api.gateway_id and coordinator.api.reboot:
|
| 36 |
+
entities.append(PlugwiseButtonEntity(coordinator, device_id))
|
| 37 |
+
LOGGER.debug("Add %s reboot button", device["name"])
|
| 38 |
+
async_add_entities(entities)
|
| 39 |
|
| 40 |
|
| 41 |
class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
|
|
|
|
| 55 |
self._attr_unique_id = f"{device_id}-reboot"
|
| 56 |
|
| 57 |
@plugwise_command
|
|
|
|
| 58 |
async def async_press(self) -> None:
|
| 59 |
"""Triggers the Plugwise button press service."""
|
| 60 |
await self.coordinator.api.reboot_gateway()
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""Plugwise Climate component for Home Assistant."""
|
| 2 |
|
| 3 |
from dataclasses import asdict, dataclass
|
| 4 |
-
from typing import Any
|
| 5 |
|
| 6 |
from homeassistant.components.climate import (
|
| 7 |
ATTR_HVAC_MODE,
|
|
@@ -12,13 +12,39 @@
|
|
| 12 |
HVACAction,
|
| 13 |
HVACMode,
|
| 14 |
)
|
| 15 |
-
from homeassistant.const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
from homeassistant.core import HomeAssistant, callback
|
| 17 |
from homeassistant.exceptions import HomeAssistantError
|
| 18 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 19 |
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
|
| 20 |
|
| 21 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 23 |
from .entity import PlugwiseEntity
|
| 24 |
from .util import plugwise_command
|
|
@@ -27,6 +53,42 @@
|
|
| 27 |
PARALLEL_UPDATES = 0
|
| 28 |
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
def _check_for_schedule(active: bool, last_active: str | None) -> None:
|
| 31 |
"""Raise a HAError when no thermostat schedule has been set."""
|
| 32 |
if not active and last_active is None:
|
|
@@ -43,57 +105,19 @@
|
|
| 43 |
last_active_schedule: str | None
|
| 44 |
previous_action_mode: str | None
|
| 45 |
|
| 46 |
-
@override
|
| 47 |
def as_dict(self) -> dict[str, Any]:
|
| 48 |
"""Return a dict representation of the text data."""
|
| 49 |
return asdict(self)
|
| 50 |
|
| 51 |
-
@classmethod
|
| 52 |
-
def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData:
|
| 53 |
-
"""Initialize a stored data object from a dict."""
|
| 54 |
-
return cls(
|
| 55 |
-
last_active_schedule=restored.get("last_active_schedule"),
|
| 56 |
-
previous_action_mode=restored.get("previous_action_mode"),
|
| 57 |
-
)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
async def async_setup_entry(
|
| 61 |
-
hass: HomeAssistant,
|
| 62 |
-
entry: PlugwiseConfigEntry,
|
| 63 |
-
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 64 |
-
) -> None:
|
| 65 |
-
"""Set up the Smile Thermostats from a config entry."""
|
| 66 |
-
coordinator = entry.runtime_data
|
| 67 |
-
|
| 68 |
-
@callback
|
| 69 |
-
def _add_entities() -> None:
|
| 70 |
-
"""Add Entities."""
|
| 71 |
-
if not coordinator.new_devices:
|
| 72 |
-
return
|
| 73 |
-
|
| 74 |
-
if coordinator.api.smile.name == "Adam":
|
| 75 |
-
async_add_entities(
|
| 76 |
-
PlugwiseClimateEntity(coordinator, device_id)
|
| 77 |
-
for device_id in coordinator.new_devices
|
| 78 |
-
if coordinator.data[device_id]["dev_class"] == "climate"
|
| 79 |
-
)
|
| 80 |
-
else:
|
| 81 |
-
async_add_entities(
|
| 82 |
-
PlugwiseClimateEntity(coordinator, device_id)
|
| 83 |
-
for device_id in coordinator.new_devices
|
| 84 |
-
if coordinator.data[device_id]["dev_class"] in MASTER_THERMOSTATS
|
| 85 |
-
)
|
| 86 |
-
|
| 87 |
-
_add_entities()
|
| 88 |
-
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 89 |
-
|
| 90 |
|
| 91 |
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
|
| 92 |
"""Representation of a Plugwise thermostat."""
|
| 93 |
|
|
|
|
| 94 |
_attr_name = None
|
| 95 |
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
| 96 |
_attr_translation_key = DOMAIN
|
|
|
|
| 97 |
|
| 98 |
def __init__(
|
| 99 |
self,
|
|
@@ -102,20 +126,30 @@
|
|
| 102 |
) -> None:
|
| 103 |
"""Set up the Plugwise API."""
|
| 104 |
super().__init__(coordinator, device_id)
|
| 105 |
-
self._attr_unique_id = f"{device_id}-climate"
|
| 106 |
|
| 107 |
self._api = coordinator.api
|
| 108 |
gateway_id: str = self._api.gateway_id
|
| 109 |
self._gateway_data = coordinator.data[gateway_id]
|
| 110 |
self._last_active_schedule: str | None = None
|
| 111 |
self._location = device_id
|
| 112 |
-
if (location := self.device.get(
|
| 113 |
self._location = location
|
| 114 |
self._previous_action_mode = HVACAction.HEATING.value
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
# Determine supported features
|
| 117 |
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
| 118 |
-
if
|
|
|
|
|
|
|
|
|
|
| 119 |
self._attr_supported_features = (
|
| 120 |
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
| 121 |
)
|
|
@@ -123,100 +157,91 @@
|
|
| 123 |
self._attr_supported_features |= (
|
| 124 |
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
| 125 |
)
|
| 126 |
-
if presets := self.device.get("preset_modes"):
|
| 127 |
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
self._attr_min_temp = self.device["thermostat"]["lower_bound"]
|
| 131 |
-
self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
|
| 132 |
-
# Ensure we don't drop below 0.1
|
| 133 |
-
self._attr_target_temperature_step = max(
|
| 134 |
-
self.device["thermostat"]["resolution"], 0.1
|
| 135 |
-
)
|
| 136 |
|
| 137 |
-
@override
|
| 138 |
async def async_added_to_hass(self) -> None:
|
| 139 |
"""Run when entity about to be added."""
|
| 140 |
-
await super().async_added_to_hass()
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
)
|
| 146 |
-
self._last_active_schedule = plugwise_extra_data.last_active_schedule
|
| 147 |
self._previous_action_mode = (
|
| 148 |
-
|
| 149 |
)
|
| 150 |
|
|
|
|
|
|
|
| 151 |
@property
|
| 152 |
-
@override
|
| 153 |
def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
|
| 154 |
"""Return text specific state data to be restored."""
|
| 155 |
return PlugwiseClimateExtraStoredData(
|
| 156 |
-
|
| 157 |
-
|
| 158 |
)
|
| 159 |
|
| 160 |
@property
|
| 161 |
-
@override
|
| 162 |
def current_temperature(self) -> float | None:
|
| 163 |
"""Return the current temperature."""
|
| 164 |
-
return self.device
|
| 165 |
|
| 166 |
@property
|
| 167 |
-
|
| 168 |
-
def target_temperature(self) -> float:
|
| 169 |
"""Return the temperature we try to reach.
|
| 170 |
|
| 171 |
Connected to the HVACMode combination of AUTO-HEAT.
|
| 172 |
"""
|
| 173 |
|
| 174 |
-
return self.device
|
| 175 |
|
| 176 |
@property
|
| 177 |
-
|
| 178 |
-
def target_temperature_high(self) -> float:
|
| 179 |
"""Return the temperature we try to reach in case of cooling.
|
| 180 |
|
| 181 |
Connected to the HVACMode combination of AUTO-HEAT_COOL.
|
| 182 |
"""
|
| 183 |
-
return self.device
|
| 184 |
|
| 185 |
@property
|
| 186 |
-
|
| 187 |
-
def target_temperature_low(self) -> float:
|
| 188 |
"""Return the heating temperature we try to reach in case of heating.
|
| 189 |
|
| 190 |
Connected to the HVACMode combination AUTO-HEAT_COOL.
|
| 191 |
"""
|
| 192 |
-
return self.device
|
| 193 |
|
| 194 |
@property
|
| 195 |
-
@override
|
| 196 |
def hvac_mode(self) -> HVACMode:
|
| 197 |
"""Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
@property
|
| 205 |
-
@override
|
| 206 |
def hvac_modes(self) -> list[HVACMode]:
|
| 207 |
"""Return a list of available HVACModes."""
|
| 208 |
hvac_modes: list[HVACMode] = []
|
| 209 |
-
if
|
| 210 |
hvac_modes.append(HVACMode.OFF)
|
| 211 |
|
| 212 |
-
if self.device.get(
|
| 213 |
hvac_modes.append(HVACMode.AUTO)
|
| 214 |
|
| 215 |
if self._api.cooling_present:
|
| 216 |
-
if
|
| 217 |
-
if "heating" in self._gateway_data[
|
| 218 |
hvac_modes.append(HVACMode.HEAT)
|
| 219 |
-
if "cooling" in self._gateway_data[
|
| 220 |
hvac_modes.append(HVACMode.COOL)
|
| 221 |
else:
|
| 222 |
hvac_modes.append(HVACMode.HEAT_COOL)
|
|
@@ -226,41 +251,40 @@
|
|
| 226 |
return hvac_modes
|
| 227 |
|
| 228 |
@property
|
| 229 |
-
|
| 230 |
-
def hvac_action(self) -> HVACAction:
|
| 231 |
"""Return the current running hvac operation if supported."""
|
| 232 |
# Keep track of the previous hvac_action mode.
|
| 233 |
# When no cooling available, _previous_action_mode is always heating
|
| 234 |
if (
|
| 235 |
-
|
| 236 |
-
and HVACAction.COOLING.value in self._gateway_data[
|
| 237 |
):
|
| 238 |
-
mode = self._gateway_data[
|
| 239 |
if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
|
| 240 |
self._previous_action_mode = mode
|
| 241 |
|
| 242 |
-
if (action := self.device.get(
|
| 243 |
return HVACAction(action)
|
| 244 |
|
| 245 |
return HVACAction.IDLE
|
| 246 |
|
| 247 |
@property
|
| 248 |
-
@override
|
| 249 |
def preset_mode(self) -> str | None:
|
| 250 |
"""Return the current preset mode."""
|
| 251 |
-
return self.device.get(
|
| 252 |
|
| 253 |
@plugwise_command
|
| 254 |
-
@override
|
| 255 |
async def async_set_temperature(self, **kwargs: Any) -> None:
|
| 256 |
"""Set new target temperature."""
|
| 257 |
data: dict[str, Any] = {}
|
| 258 |
if ATTR_TEMPERATURE in kwargs:
|
| 259 |
-
data[
|
| 260 |
if ATTR_TARGET_TEMP_HIGH in kwargs:
|
| 261 |
-
data[
|
| 262 |
if ATTR_TARGET_TEMP_LOW in kwargs:
|
| 263 |
-
data[
|
|
|
|
|
|
|
| 264 |
|
| 265 |
if mode := kwargs.get(ATTR_HVAC_MODE):
|
| 266 |
await self.async_set_hvac_mode(mode)
|
|
@@ -268,7 +292,7 @@
|
|
| 268 |
await self._api.set_temperature(self._location, data)
|
| 269 |
|
| 270 |
def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
|
| 271 |
-
"""Return the API regulation value for a manual HVAC mode
|
| 272 |
|
| 273 |
The function inputs are limited to the HVACModes HEAT and COOL.
|
| 274 |
"""
|
|
@@ -279,9 +303,9 @@
|
|
| 279 |
return mode
|
| 280 |
|
| 281 |
@plugwise_command
|
| 282 |
-
@override
|
| 283 |
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
| 284 |
"""Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
|
|
|
|
| 285 |
# Early exit if no mode change
|
| 286 |
if hvac_mode == self.hvac_mode:
|
| 287 |
return
|
|
@@ -329,7 +353,6 @@
|
|
| 329 |
await self._api.set_schedule_state(self._location, STATE_ON, desired_schedule)
|
| 330 |
|
| 331 |
@plugwise_command
|
| 332 |
-
@override
|
| 333 |
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
| 334 |
"""Set the preset mode."""
|
| 335 |
await self._api.set_preset(self._location, preset_mode)
|
|
|
|
| 1 |
"""Plugwise Climate component for Home Assistant."""
|
| 2 |
|
| 3 |
from dataclasses import asdict, dataclass
|
| 4 |
+
from typing import Any
|
| 5 |
|
| 6 |
from homeassistant.components.climate import (
|
| 7 |
ATTR_HVAC_MODE,
|
|
|
|
| 12 |
HVACAction,
|
| 13 |
HVACMode,
|
| 14 |
)
|
| 15 |
+
from homeassistant.const import (
|
| 16 |
+
ATTR_NAME,
|
| 17 |
+
ATTR_TEMPERATURE,
|
| 18 |
+
STATE_OFF,
|
| 19 |
+
STATE_ON,
|
| 20 |
+
UnitOfTemperature,
|
| 21 |
+
)
|
| 22 |
from homeassistant.core import HomeAssistant, callback
|
| 23 |
from homeassistant.exceptions import HomeAssistantError
|
| 24 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 25 |
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
|
| 26 |
|
| 27 |
+
from .const import (
|
| 28 |
+
ACTIVE_PRESET,
|
| 29 |
+
AVAILABLE_SCHEDULES,
|
| 30 |
+
CLIMATE_MODE,
|
| 31 |
+
CONTROL_STATE,
|
| 32 |
+
DEV_CLASS,
|
| 33 |
+
DOMAIN,
|
| 34 |
+
LOCATION,
|
| 35 |
+
LOGGER,
|
| 36 |
+
LOWER_BOUND,
|
| 37 |
+
MASTER_THERMOSTATS,
|
| 38 |
+
REGULATION_MODES,
|
| 39 |
+
RESOLUTION,
|
| 40 |
+
SELECT_REGULATION_MODE,
|
| 41 |
+
SENSORS,
|
| 42 |
+
TARGET_TEMP,
|
| 43 |
+
TARGET_TEMP_HIGH,
|
| 44 |
+
TARGET_TEMP_LOW,
|
| 45 |
+
THERMOSTAT,
|
| 46 |
+
UPPER_BOUND,
|
| 47 |
+
)
|
| 48 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 49 |
from .entity import PlugwiseEntity
|
| 50 |
from .util import plugwise_command
|
|
|
|
| 53 |
PARALLEL_UPDATES = 0
|
| 54 |
|
| 55 |
|
| 56 |
+
async def async_setup_entry(
|
| 57 |
+
hass: HomeAssistant,
|
| 58 |
+
entry: PlugwiseConfigEntry,
|
| 59 |
+
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 60 |
+
) -> None:
|
| 61 |
+
"""Set up Plugwise thermostats from a config entry."""
|
| 62 |
+
coordinator = entry.runtime_data
|
| 63 |
+
|
| 64 |
+
@callback
|
| 65 |
+
def _add_entities() -> None:
|
| 66 |
+
"""Add Entities during init and runtime."""
|
| 67 |
+
if not coordinator.new_devices:
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
entities: list[PlugwiseClimateEntity] = []
|
| 71 |
+
gateway_name = coordinator.api.smile.name
|
| 72 |
+
for device_id in coordinator.new_devices:
|
| 73 |
+
device = coordinator.data[device_id]
|
| 74 |
+
if gateway_name == "Adam":
|
| 75 |
+
if device[DEV_CLASS] == "climate":
|
| 76 |
+
entities.append(
|
| 77 |
+
PlugwiseClimateEntity(coordinator, device_id)
|
| 78 |
+
)
|
| 79 |
+
LOGGER.debug("Add climate %s", device[ATTR_NAME])
|
| 80 |
+
elif device[DEV_CLASS] in MASTER_THERMOSTATS:
|
| 81 |
+
entities.append(
|
| 82 |
+
PlugwiseClimateEntity(coordinator, device_id)
|
| 83 |
+
)
|
| 84 |
+
LOGGER.debug("Add climate %s", device[ATTR_NAME])
|
| 85 |
+
|
| 86 |
+
async_add_entities(entities)
|
| 87 |
+
|
| 88 |
+
_add_entities()
|
| 89 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 90 |
+
|
| 91 |
+
|
| 92 |
def _check_for_schedule(active: bool, last_active: str | None) -> None:
|
| 93 |
"""Raise a HAError when no thermostat schedule has been set."""
|
| 94 |
if not active and last_active is None:
|
|
|
|
| 105 |
last_active_schedule: str | None
|
| 106 |
previous_action_mode: str | None
|
| 107 |
|
|
|
|
| 108 |
def as_dict(self) -> dict[str, Any]:
|
| 109 |
"""Return a dict representation of the text data."""
|
| 110 |
return asdict(self)
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
|
| 114 |
"""Representation of a Plugwise thermostat."""
|
| 115 |
|
| 116 |
+
_attr_has_entity_name = True
|
| 117 |
_attr_name = None
|
| 118 |
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
| 119 |
_attr_translation_key = DOMAIN
|
| 120 |
+
_enable_turn_on_off_backwards_compatibility = False
|
| 121 |
|
| 122 |
def __init__(
|
| 123 |
self,
|
|
|
|
| 126 |
) -> None:
|
| 127 |
"""Set up the Plugwise API."""
|
| 128 |
super().__init__(coordinator, device_id)
|
|
|
|
| 129 |
|
| 130 |
self._api = coordinator.api
|
| 131 |
gateway_id: str = self._api.gateway_id
|
| 132 |
self._gateway_data = coordinator.data[gateway_id]
|
| 133 |
self._last_active_schedule: str | None = None
|
| 134 |
self._location = device_id
|
| 135 |
+
if (location := self.device.get(LOCATION)) is not None:
|
| 136 |
self._location = location
|
| 137 |
self._previous_action_mode = HVACAction.HEATING.value
|
| 138 |
|
| 139 |
+
self._attr_max_temp = min(self.device.get(THERMOSTAT, {}).get(UPPER_BOUND, 35.0), 35.0)
|
| 140 |
+
self._attr_min_temp = self.device.get(THERMOSTAT, {}).get(LOWER_BOUND, 0.0)
|
| 141 |
+
# Ensure we don't drop below 0.1
|
| 142 |
+
self._attr_target_temperature_step = max(
|
| 143 |
+
self.device.get(THERMOSTAT, {}).get(RESOLUTION, 0.5), 0.1
|
| 144 |
+
)
|
| 145 |
+
self._attr_unique_id = f"{device_id}-climate"
|
| 146 |
+
|
| 147 |
# Determine supported features
|
| 148 |
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
| 149 |
+
if (
|
| 150 |
+
self._api.cooling_present
|
| 151 |
+
and self._api.smile.name != "Adam"
|
| 152 |
+
):
|
| 153 |
self._attr_supported_features = (
|
| 154 |
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
| 155 |
)
|
|
|
|
| 157 |
self._attr_supported_features |= (
|
| 158 |
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
| 159 |
)
|
| 160 |
+
if presets := self.device.get("preset_modes", None): # can be NONE
|
| 161 |
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
| 162 |
+
self._attr_preset_modes = presets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
|
|
|
| 164 |
async def async_added_to_hass(self) -> None:
|
| 165 |
"""Run when entity about to be added."""
|
|
|
|
| 166 |
|
| 167 |
+
extra_data = await self.async_get_last_extra_data()
|
| 168 |
+
if extra_data is not None:
|
| 169 |
+
data = extra_data.as_dict()
|
| 170 |
+
self._last_active_schedule = data.get("last_active_schedule")
|
|
|
|
| 171 |
self._previous_action_mode = (
|
| 172 |
+
data.get("previous_action_mode") or HVACAction.HEATING.value
|
| 173 |
)
|
| 174 |
|
| 175 |
+
await super().async_added_to_hass()
|
| 176 |
+
|
| 177 |
@property
|
|
|
|
| 178 |
def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
|
| 179 |
"""Return text specific state data to be restored."""
|
| 180 |
return PlugwiseClimateExtraStoredData(
|
| 181 |
+
self._last_active_schedule,
|
| 182 |
+
self._previous_action_mode,
|
| 183 |
)
|
| 184 |
|
| 185 |
@property
|
|
|
|
| 186 |
def current_temperature(self) -> float | None:
|
| 187 |
"""Return the current temperature."""
|
| 188 |
+
return self.device.get(SENSORS, {}).get(ATTR_TEMPERATURE)
|
| 189 |
|
| 190 |
@property
|
| 191 |
+
def target_temperature(self) -> float | None:
|
|
|
|
| 192 |
"""Return the temperature we try to reach.
|
| 193 |
|
| 194 |
Connected to the HVACMode combination of AUTO-HEAT.
|
| 195 |
"""
|
| 196 |
|
| 197 |
+
return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP)
|
| 198 |
|
| 199 |
@property
|
| 200 |
+
def target_temperature_high(self) -> float | None:
|
|
|
|
| 201 |
"""Return the temperature we try to reach in case of cooling.
|
| 202 |
|
| 203 |
Connected to the HVACMode combination of AUTO-HEAT_COOL.
|
| 204 |
"""
|
| 205 |
+
return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP_HIGH)
|
| 206 |
|
| 207 |
@property
|
| 208 |
+
def target_temperature_low(self) -> float | None:
|
|
|
|
| 209 |
"""Return the heating temperature we try to reach in case of heating.
|
| 210 |
|
| 211 |
Connected to the HVACMode combination AUTO-HEAT_COOL.
|
| 212 |
"""
|
| 213 |
+
return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP_LOW)
|
| 214 |
|
| 215 |
@property
|
|
|
|
| 216 |
def hvac_mode(self) -> HVACMode:
|
| 217 |
"""Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
|
| 218 |
+
mode = self.device.get(CLIMATE_MODE)
|
| 219 |
+
if mode is None:
|
| 220 |
+
return HVACMode.HEAT # pragma: no cover
|
| 221 |
+
try:
|
| 222 |
+
hvac = HVACMode(mode)
|
| 223 |
+
except ValueError: # pragma: no cover
|
| 224 |
+
return HVACMode.HEAT # pragma: no cover
|
| 225 |
+
if hvac not in self.hvac_modes:
|
| 226 |
+
return HVACMode.HEAT # pragma: no cover
|
| 227 |
+
|
| 228 |
+
return hvac
|
| 229 |
|
| 230 |
@property
|
|
|
|
| 231 |
def hvac_modes(self) -> list[HVACMode]:
|
| 232 |
"""Return a list of available HVACModes."""
|
| 233 |
hvac_modes: list[HVACMode] = []
|
| 234 |
+
if REGULATION_MODES in self._gateway_data:
|
| 235 |
hvac_modes.append(HVACMode.OFF)
|
| 236 |
|
| 237 |
+
if self.device.get(AVAILABLE_SCHEDULES, []):
|
| 238 |
hvac_modes.append(HVACMode.AUTO)
|
| 239 |
|
| 240 |
if self._api.cooling_present:
|
| 241 |
+
if REGULATION_MODES in self._gateway_data:
|
| 242 |
+
if "heating" in self._gateway_data[REGULATION_MODES]:
|
| 243 |
hvac_modes.append(HVACMode.HEAT)
|
| 244 |
+
if "cooling" in self._gateway_data[REGULATION_MODES]:
|
| 245 |
hvac_modes.append(HVACMode.COOL)
|
| 246 |
else:
|
| 247 |
hvac_modes.append(HVACMode.HEAT_COOL)
|
|
|
|
| 251 |
return hvac_modes
|
| 252 |
|
| 253 |
@property
|
| 254 |
+
def hvac_action(self) -> HVACAction: # pw-beta add to Core
|
|
|
|
| 255 |
"""Return the current running hvac operation if supported."""
|
| 256 |
# Keep track of the previous hvac_action mode.
|
| 257 |
# When no cooling available, _previous_action_mode is always heating
|
| 258 |
if (
|
| 259 |
+
REGULATION_MODES in self._gateway_data
|
| 260 |
+
and HVACAction.COOLING.value in self._gateway_data[REGULATION_MODES]
|
| 261 |
):
|
| 262 |
+
mode = self._gateway_data[SELECT_REGULATION_MODE]
|
| 263 |
if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
|
| 264 |
self._previous_action_mode = mode
|
| 265 |
|
| 266 |
+
if (action := self.device.get(CONTROL_STATE)) is not None:
|
| 267 |
return HVACAction(action)
|
| 268 |
|
| 269 |
return HVACAction.IDLE
|
| 270 |
|
| 271 |
@property
|
|
|
|
| 272 |
def preset_mode(self) -> str | None:
|
| 273 |
"""Return the current preset mode."""
|
| 274 |
+
return self.device.get(ACTIVE_PRESET)
|
| 275 |
|
| 276 |
@plugwise_command
|
|
|
|
| 277 |
async def async_set_temperature(self, **kwargs: Any) -> None:
|
| 278 |
"""Set new target temperature."""
|
| 279 |
data: dict[str, Any] = {}
|
| 280 |
if ATTR_TEMPERATURE in kwargs:
|
| 281 |
+
data[TARGET_TEMP] = kwargs.get(ATTR_TEMPERATURE)
|
| 282 |
if ATTR_TARGET_TEMP_HIGH in kwargs:
|
| 283 |
+
data[TARGET_TEMP_HIGH] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
| 284 |
if ATTR_TARGET_TEMP_LOW in kwargs:
|
| 285 |
+
data[TARGET_TEMP_LOW] = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
| 286 |
+
|
| 287 |
+
# Upstream removed input-valid check
|
| 288 |
|
| 289 |
if mode := kwargs.get(ATTR_HVAC_MODE):
|
| 290 |
await self.async_set_hvac_mode(mode)
|
|
|
|
| 292 |
await self._api.set_temperature(self._location, data)
|
| 293 |
|
| 294 |
def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
|
| 295 |
+
"""Return the API regulation value for a manual HVAC mode.
|
| 296 |
|
| 297 |
The function inputs are limited to the HVACModes HEAT and COOL.
|
| 298 |
"""
|
|
|
|
| 303 |
return mode
|
| 304 |
|
| 305 |
@plugwise_command
|
|
|
|
| 306 |
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
| 307 |
"""Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
|
| 308 |
+
|
| 309 |
# Early exit if no mode change
|
| 310 |
if hvac_mode == self.hvac_mode:
|
| 311 |
return
|
|
|
|
| 353 |
await self._api.set_schedule_state(self._location, STATE_ON, desired_schedule)
|
| 354 |
|
| 355 |
@plugwise_command
|
|
|
|
| 356 |
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
| 357 |
"""Set the preset mode."""
|
| 358 |
await self._api.set_preset(self._location, preset_mode)
|
|
@@ -1,7 +1,10 @@
|
|
| 1 |
"""Config flow for Plugwise integration."""
|
| 2 |
|
|
|
|
|
|
|
|
|
|
| 3 |
import logging
|
| 4 |
-
from typing import Any, Self
|
| 5 |
|
| 6 |
from plugwise import Smile
|
| 7 |
from plugwise.exceptions import (
|
|
@@ -14,7 +17,15 @@
|
|
| 14 |
)
|
| 15 |
import voluptuous as vol
|
| 16 |
|
| 17 |
-
from homeassistant.config_entries import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
from homeassistant.const import (
|
| 19 |
ATTR_CONFIGURATION_URL,
|
| 20 |
CONF_BASE,
|
|
@@ -22,9 +33,13 @@
|
|
| 22 |
CONF_NAME,
|
| 23 |
CONF_PASSWORD,
|
| 24 |
CONF_PORT,
|
|
|
|
| 25 |
CONF_USERNAME,
|
| 26 |
)
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
| 28 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
| 29 |
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
| 30 |
|
|
@@ -30,22 +45,35 @@
|
|
| 30 |
|
| 31 |
from .const import (
|
| 32 |
ANNA_WITH_ADAM,
|
|
|
|
| 33 |
DEFAULT_PORT,
|
|
|
|
| 34 |
DEFAULT_USERNAME,
|
| 35 |
DOMAIN,
|
| 36 |
FLOW_SMILE,
|
| 37 |
FLOW_STRETCH,
|
|
|
|
|
|
|
| 38 |
SMILE,
|
| 39 |
SMILE_OPEN_THERM,
|
| 40 |
SMILE_THERMO,
|
| 41 |
STRETCH,
|
| 42 |
STRETCH_USERNAME,
|
| 43 |
-
|
|
|
|
|
|
|
| 44 |
ZEROCONF_MAP,
|
| 45 |
)
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
_LOGGER = logging.getLogger(__name__)
|
| 48 |
|
|
|
|
|
|
|
| 49 |
SMILE_RECONF_SCHEMA = vol.Schema(
|
| 50 |
{
|
| 51 |
vol.Required(CONF_HOST): str,
|
|
@@ -53,21 +81,31 @@
|
|
| 53 |
)
|
| 54 |
|
| 55 |
|
| 56 |
-
def smile_user_schema(
|
| 57 |
"""Generate base schema for gateways."""
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
if not discovery_info:
|
| 61 |
-
schema = schema.extend(
|
| 62 |
{
|
| 63 |
vol.Required(CONF_HOST): str,
|
|
|
|
| 64 |
vol.Required(CONF_USERNAME, default=SMILE): vol.In(
|
| 65 |
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
| 66 |
),
|
| 67 |
}
|
| 68 |
)
|
| 69 |
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
|
| 73 |
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
|
|
@@ -101,7 +139,7 @@
|
|
| 101 |
errors[CONF_BASE] = "invalid_auth"
|
| 102 |
except InvalidSetupError:
|
| 103 |
errors[CONF_BASE] = "invalid_setup"
|
| 104 |
-
except InvalidXMLError, ResponseError:
|
| 105 |
errors[CONF_BASE] = "response_error"
|
| 106 |
except UnsupportedDeviceError:
|
| 107 |
errors[CONF_BASE] = "unsupported"
|
|
@@ -117,29 +155,33 @@
|
|
| 117 |
"""Handle a config flow for Plugwise Smile."""
|
| 118 |
|
| 119 |
VERSION = 1
|
|
|
|
| 120 |
|
| 121 |
discovery_info: ZeroconfServiceInfo | None = None
|
| 122 |
-
product: str =
|
| 123 |
_username: str = DEFAULT_USERNAME
|
| 124 |
|
| 125 |
-
@override
|
| 126 |
async def async_step_zeroconf(
|
| 127 |
self, discovery_info: ZeroconfServiceInfo
|
| 128 |
) -> ConfigFlowResult:
|
| 129 |
"""Prepare configuration for a discovered Plugwise Smile."""
|
| 130 |
self.discovery_info = discovery_info
|
| 131 |
_properties = discovery_info.properties
|
| 132 |
-
|
|
|
|
| 133 |
unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
|
|
|
|
|
|
|
|
|
|
| 134 |
if config_entry := await self.async_set_unique_id(unique_id):
|
| 135 |
try:
|
| 136 |
await validate_input(
|
| 137 |
self.hass,
|
| 138 |
{
|
| 139 |
CONF_HOST: discovery_info.host,
|
|
|
|
| 140 |
CONF_PORT: discovery_info.port,
|
| 141 |
CONF_USERNAME: config_entry.data[CONF_USERNAME],
|
| 142 |
-
CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
|
| 143 |
},
|
| 144 |
)
|
| 145 |
except Exception: # noqa: BLE001
|
|
@@ -152,12 +194,6 @@
|
|
| 152 |
}
|
| 153 |
)
|
| 154 |
|
| 155 |
-
if DEFAULT_USERNAME not in unique_id:
|
| 156 |
-
self._username = STRETCH_USERNAME
|
| 157 |
-
self.product = _product = _properties.get("product", UNKNOWN_SMILE)
|
| 158 |
-
_version = _properties.get("version", "n/a")
|
| 159 |
-
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
|
| 160 |
-
|
| 161 |
# This is an Anna, but we already have config entries.
|
| 162 |
# Assuming that the user has already configured Adam, aborting discovery.
|
| 163 |
if self._async_current_entries() and _product == SMILE_THERMO:
|
|
@@ -167,19 +203,19 @@
|
|
| 167 |
# In that case, we need to cancel the Anna flow, as the Adam should
|
| 168 |
# be added.
|
| 169 |
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
| 170 |
-
return self.async_abort(reason=
|
| 171 |
|
|
|
|
| 172 |
self.context.update(
|
| 173 |
{
|
| 174 |
-
|
| 175 |
ATTR_CONFIGURATION_URL: (
|
| 176 |
f"http://{discovery_info.host}:{discovery_info.port}"
|
| 177 |
-
)
|
| 178 |
}
|
| 179 |
)
|
| 180 |
return await self.async_step_user()
|
| 181 |
|
| 182 |
-
@override
|
| 183 |
def is_matching(self, other_flow: Self) -> bool:
|
| 184 |
"""Return True if other_flow is matching this flow."""
|
| 185 |
# This is an Anna, and there is already an Adam flow in progress
|
|
@@ -192,7 +228,7 @@
|
|
| 192 |
|
| 193 |
return False
|
| 194 |
|
| 195 |
-
|
| 196 |
async def async_step_user(
|
| 197 |
self, user_input: dict[str, Any] | None = None
|
| 198 |
) -> ConfigFlowResult:
|
|
@@ -209,18 +245,19 @@
|
|
| 209 |
api, errors = await verify_connection(self.hass, user_input)
|
| 210 |
if api:
|
| 211 |
await self.async_set_unique_id(
|
| 212 |
-
api.smile.hostname or api.gateway_id,
|
| 213 |
-
raise_on_progress=False,
|
| 214 |
)
|
| 215 |
self._abort_if_unique_id_configured()
|
| 216 |
return self.async_create_entry(title=api.smile.name, data=user_input)
|
| 217 |
|
|
|
|
| 218 |
return self.async_show_form(
|
| 219 |
step_id=SOURCE_USER,
|
| 220 |
-
data_schema=smile_user_schema(
|
| 221 |
errors=errors,
|
| 222 |
)
|
| 223 |
|
|
|
|
| 224 |
async def async_step_reconfigure(
|
| 225 |
self, user_input: dict[str, Any] | None = None
|
| 226 |
) -> ConfigFlowResult:
|
|
@@ -230,7 +267,7 @@
|
|
| 230 |
reconfigure_entry = self._get_reconfigure_entry()
|
| 231 |
|
| 232 |
if user_input:
|
| 233 |
-
#
|
| 234 |
full_input = {
|
| 235 |
CONF_HOST: user_input.get(CONF_HOST),
|
| 236 |
CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
|
|
@@ -241,8 +278,7 @@
|
|
| 241 |
api, errors = await verify_connection(self.hass, full_input)
|
| 242 |
if api:
|
| 243 |
await self.async_set_unique_id(
|
| 244 |
-
api.smile.hostname or api.gateway_id,
|
| 245 |
-
raise_on_progress=False,
|
| 246 |
)
|
| 247 |
self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
|
| 248 |
return self.async_update_reload_and_abort(
|
|
@@ -259,3 +295,66 @@
|
|
| 259 |
description_placeholders={"title": reconfigure_entry.title},
|
| 260 |
errors=errors,
|
| 261 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Config flow for Plugwise integration."""
|
| 2 |
|
| 3 |
+
# pylint: disable=home-assistant-config-flow-polling-field
|
| 4 |
+
|
| 5 |
+
from copy import deepcopy
|
| 6 |
import logging
|
| 7 |
+
from typing import Any, Self
|
| 8 |
|
| 9 |
from plugwise import Smile
|
| 10 |
from plugwise.exceptions import (
|
|
|
|
| 17 |
)
|
| 18 |
import voluptuous as vol
|
| 19 |
|
| 20 |
+
from homeassistant.config_entries import (
|
| 21 |
+
SOURCE_USER,
|
| 22 |
+
ConfigEntry,
|
| 23 |
+
ConfigFlow,
|
| 24 |
+
ConfigFlowResult,
|
| 25 |
+
OptionsFlow,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Upstream
|
| 29 |
from homeassistant.const import (
|
| 30 |
ATTR_CONFIGURATION_URL,
|
| 31 |
CONF_BASE,
|
|
|
|
| 33 |
CONF_NAME,
|
| 34 |
CONF_PASSWORD,
|
| 35 |
CONF_PORT,
|
| 36 |
+
CONF_SCAN_INTERVAL,
|
| 37 |
CONF_USERNAME,
|
| 38 |
)
|
| 39 |
+
|
| 40 |
+
# Upstream
|
| 41 |
+
from homeassistant.core import HomeAssistant, callback
|
| 42 |
+
from homeassistant.helpers import config_validation as cv
|
| 43 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
| 44 |
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
| 45 |
|
|
|
|
| 45 |
|
| 46 |
from .const import (
|
| 47 |
ANNA_WITH_ADAM,
|
| 48 |
+
CONF_REFRESH_INTERVAL, # pw-beta option
|
| 49 |
DEFAULT_PORT,
|
| 50 |
+
DEFAULT_UPDATE_INTERVAL,
|
| 51 |
DEFAULT_USERNAME,
|
| 52 |
DOMAIN,
|
| 53 |
FLOW_SMILE,
|
| 54 |
FLOW_STRETCH,
|
| 55 |
+
INIT,
|
| 56 |
+
P1_UPDATE_INTERVAL,
|
| 57 |
SMILE,
|
| 58 |
SMILE_OPEN_THERM,
|
| 59 |
SMILE_THERMO,
|
| 60 |
STRETCH,
|
| 61 |
STRETCH_USERNAME,
|
| 62 |
+
THERMOSTAT,
|
| 63 |
+
TITLE_PLACEHOLDERS,
|
| 64 |
+
VERSION,
|
| 65 |
ZEROCONF_MAP,
|
| 66 |
)
|
| 67 |
|
| 68 |
+
# Upstream
|
| 69 |
+
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 70 |
+
|
| 71 |
+
type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
|
| 72 |
+
|
| 73 |
_LOGGER = logging.getLogger(__name__)
|
| 74 |
|
| 75 |
+
# Upstream basically the whole file (excluding the pw-beta options)
|
| 76 |
+
|
| 77 |
SMILE_RECONF_SCHEMA = vol.Schema(
|
| 78 |
{
|
| 79 |
vol.Required(CONF_HOST): str,
|
|
|
|
| 81 |
)
|
| 82 |
|
| 83 |
|
| 84 |
+
def smile_user_schema(cf_input: ZeroconfServiceInfo | dict[str, Any] | None) -> vol.Schema:
|
| 85 |
"""Generate base schema for gateways."""
|
| 86 |
+
if not cf_input: # no discovery- or user-input available
|
| 87 |
+
return vol.Schema(
|
|
|
|
|
|
|
| 88 |
{
|
| 89 |
vol.Required(CONF_HOST): str,
|
| 90 |
+
vol.Required(CONF_PASSWORD): str,
|
| 91 |
vol.Required(CONF_USERNAME, default=SMILE): vol.In(
|
| 92 |
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
| 93 |
),
|
| 94 |
}
|
| 95 |
)
|
| 96 |
|
| 97 |
+
if isinstance(cf_input, ZeroconfServiceInfo):
|
| 98 |
+
return vol.Schema({vol.Required(CONF_PASSWORD): str})
|
| 99 |
+
|
| 100 |
+
return vol.Schema(
|
| 101 |
+
{
|
| 102 |
+
vol.Required(CONF_HOST, default=cf_input[CONF_HOST]): str,
|
| 103 |
+
vol.Required(CONF_PASSWORD, default=cf_input[CONF_PASSWORD]): str,
|
| 104 |
+
vol.Required(CONF_USERNAME, default=cf_input[CONF_USERNAME]): vol.In(
|
| 105 |
+
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
| 106 |
+
),
|
| 107 |
+
}
|
| 108 |
+
)
|
| 109 |
|
| 110 |
|
| 111 |
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
|
|
|
|
| 139 |
errors[CONF_BASE] = "invalid_auth"
|
| 140 |
except InvalidSetupError:
|
| 141 |
errors[CONF_BASE] = "invalid_setup"
|
| 142 |
+
except (InvalidXMLError, ResponseError):
|
| 143 |
errors[CONF_BASE] = "response_error"
|
| 144 |
except UnsupportedDeviceError:
|
| 145 |
errors[CONF_BASE] = "unsupported"
|
|
|
|
| 155 |
"""Handle a config flow for Plugwise Smile."""
|
| 156 |
|
| 157 |
VERSION = 1
|
| 158 |
+
MINOR_VERSION = 1
|
| 159 |
|
| 160 |
discovery_info: ZeroconfServiceInfo | None = None
|
| 161 |
+
product: str = "Unknown Smile"
|
| 162 |
_username: str = DEFAULT_USERNAME
|
| 163 |
|
|
|
|
| 164 |
async def async_step_zeroconf(
|
| 165 |
self, discovery_info: ZeroconfServiceInfo
|
| 166 |
) -> ConfigFlowResult:
|
| 167 |
"""Prepare configuration for a discovered Plugwise Smile."""
|
| 168 |
self.discovery_info = discovery_info
|
| 169 |
_properties = discovery_info.properties
|
| 170 |
+
_version = _properties.get(VERSION, "n/a")
|
| 171 |
+
self.product = _product = _properties.get("product", "Unknown Smile")
|
| 172 |
unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
|
| 173 |
+
if DEFAULT_USERNAME not in unique_id:
|
| 174 |
+
self._username = STRETCH_USERNAME
|
| 175 |
+
|
| 176 |
if config_entry := await self.async_set_unique_id(unique_id):
|
| 177 |
try:
|
| 178 |
await validate_input(
|
| 179 |
self.hass,
|
| 180 |
{
|
| 181 |
CONF_HOST: discovery_info.host,
|
| 182 |
+
CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
|
| 183 |
CONF_PORT: discovery_info.port,
|
| 184 |
CONF_USERNAME: config_entry.data[CONF_USERNAME],
|
|
|
|
| 185 |
},
|
| 186 |
)
|
| 187 |
except Exception: # noqa: BLE001
|
|
|
|
| 194 |
}
|
| 195 |
)
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
# This is an Anna, but we already have config entries.
|
| 198 |
# Assuming that the user has already configured Adam, aborting discovery.
|
| 199 |
if self._async_current_entries() and _product == SMILE_THERMO:
|
|
|
|
| 203 |
# In that case, we need to cancel the Anna flow, as the Adam should
|
| 204 |
# be added.
|
| 205 |
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
| 206 |
+
return self.async_abort(reason="anna_with_adam")
|
| 207 |
|
| 208 |
+
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
|
| 209 |
self.context.update(
|
| 210 |
{
|
| 211 |
+
TITLE_PLACEHOLDERS: {CONF_NAME: _name},
|
| 212 |
ATTR_CONFIGURATION_URL: (
|
| 213 |
f"http://{discovery_info.host}:{discovery_info.port}"
|
| 214 |
+
)
|
| 215 |
}
|
| 216 |
)
|
| 217 |
return await self.async_step_user()
|
| 218 |
|
|
|
|
| 219 |
def is_matching(self, other_flow: Self) -> bool:
|
| 220 |
"""Return True if other_flow is matching this flow."""
|
| 221 |
# This is an Anna, and there is already an Adam flow in progress
|
|
|
|
| 228 |
|
| 229 |
return False
|
| 230 |
|
| 231 |
+
|
| 232 |
async def async_step_user(
|
| 233 |
self, user_input: dict[str, Any] | None = None
|
| 234 |
) -> ConfigFlowResult:
|
|
|
|
| 245 |
api, errors = await verify_connection(self.hass, user_input)
|
| 246 |
if api:
|
| 247 |
await self.async_set_unique_id(
|
| 248 |
+
api.smile.hostname or api.gateway_id, raise_on_progress=False
|
|
|
|
| 249 |
)
|
| 250 |
self._abort_if_unique_id_configured()
|
| 251 |
return self.async_create_entry(title=api.smile.name, data=user_input)
|
| 252 |
|
| 253 |
+
configure_input = self.discovery_info or user_input
|
| 254 |
return self.async_show_form(
|
| 255 |
step_id=SOURCE_USER,
|
| 256 |
+
data_schema=smile_user_schema(configure_input),
|
| 257 |
errors=errors,
|
| 258 |
)
|
| 259 |
|
| 260 |
+
|
| 261 |
async def async_step_reconfigure(
|
| 262 |
self, user_input: dict[str, Any] | None = None
|
| 263 |
) -> ConfigFlowResult:
|
|
|
|
| 267 |
reconfigure_entry = self._get_reconfigure_entry()
|
| 268 |
|
| 269 |
if user_input:
|
| 270 |
+
# Redefine ingest existing username and password
|
| 271 |
full_input = {
|
| 272 |
CONF_HOST: user_input.get(CONF_HOST),
|
| 273 |
CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
|
|
|
|
| 278 |
api, errors = await verify_connection(self.hass, full_input)
|
| 279 |
if api:
|
| 280 |
await self.async_set_unique_id(
|
| 281 |
+
api.smile.hostname or api.gateway_id, raise_on_progress=False
|
|
|
|
| 282 |
)
|
| 283 |
self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
|
| 284 |
return self.async_update_reload_and_abort(
|
|
|
|
| 295 |
description_placeholders={"title": reconfigure_entry.title},
|
| 296 |
errors=errors,
|
| 297 |
)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
@staticmethod
|
| 301 |
+
@callback
|
| 302 |
+
def async_get_options_flow(
|
| 303 |
+
config_entry: PlugwiseConfigEntry,
|
| 304 |
+
) -> PlugwiseOptionsFlowHandler: # pw-beta options
|
| 305 |
+
"""Get the options flow for this handler."""
|
| 306 |
+
return PlugwiseOptionsFlowHandler(config_entry)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
# pw-beta - change the scan-interval via CONFIGURE
|
| 310 |
+
# pw-beta - change the frontend refresh interval via CONFIGURE
|
| 311 |
+
class PlugwiseOptionsFlowHandler(OptionsFlow): # pw-beta options
|
| 312 |
+
"""Plugwise option flow."""
|
| 313 |
+
|
| 314 |
+
def __init__(self, config_entry: ConfigEntry) -> None:
|
| 315 |
+
"""Initialize options flow."""
|
| 316 |
+
self.options = deepcopy(dict(config_entry.options))
|
| 317 |
+
|
| 318 |
+
def _create_options_schema(self, coordinator: PlugwiseDataUpdateCoordinator) -> vol.Schema:
|
| 319 |
+
interval = DEFAULT_UPDATE_INTERVAL
|
| 320 |
+
if coordinator.api.smile.type == "power":
|
| 321 |
+
interval = P1_UPDATE_INTERVAL
|
| 322 |
+
schema = {
|
| 323 |
+
vol.Optional(
|
| 324 |
+
CONF_SCAN_INTERVAL,
|
| 325 |
+
default=self.options.get(CONF_SCAN_INTERVAL, interval.seconds),
|
| 326 |
+
): vol.All(cv.positive_int, vol.Clamp(min=10)),
|
| 327 |
+
} # pw-beta
|
| 328 |
+
|
| 329 |
+
if coordinator.api.smile.type == THERMOSTAT:
|
| 330 |
+
schema.update({
|
| 331 |
+
vol.Optional(
|
| 332 |
+
CONF_REFRESH_INTERVAL,
|
| 333 |
+
default=self.options.get(CONF_REFRESH_INTERVAL, 1.5),
|
| 334 |
+
): vol.All(vol.Coerce(float), vol.Range(min=1.5, max=10.0)),
|
| 335 |
+
}) # pw-beta
|
| 336 |
+
|
| 337 |
+
return vol.Schema(schema)
|
| 338 |
+
|
| 339 |
+
async def async_step_none(
|
| 340 |
+
self, user_input: dict[str, Any] | None = None
|
| 341 |
+
) -> ConfigFlowResult: # pragma: no cover
|
| 342 |
+
"""No options available."""
|
| 343 |
+
if user_input is not None:
|
| 344 |
+
# Apparently not possible to abort an options flow at the moment
|
| 345 |
+
return self.async_create_entry(title="", data=self.options)
|
| 346 |
+
return self.async_show_form(step_id="none")
|
| 347 |
+
|
| 348 |
+
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
| 349 |
+
"""Manage the Plugwise options."""
|
| 350 |
+
if not self.config_entry.data.get(CONF_HOST):
|
| 351 |
+
return await self.async_step_none(user_input) # pragma: no cover
|
| 352 |
+
|
| 353 |
+
if user_input is not None:
|
| 354 |
+
return self.async_create_entry(title="", data=user_input)
|
| 355 |
+
|
| 356 |
+
coordinator = self.config_entry.runtime_data
|
| 357 |
+
return self.async_show_form(
|
| 358 |
+
step_id=INIT,
|
| 359 |
+
data_schema=self._create_options_schema(coordinator)
|
| 360 |
+
)
|
|
@@ -6,28 +6,158 @@
|
|
| 6 |
|
| 7 |
from homeassistant.const import Platform
|
| 8 |
|
|
|
|
|
|
|
| 9 |
DOMAIN: Final = "plugwise"
|
| 10 |
|
| 11 |
LOGGER = logging.getLogger(__package__)
|
| 12 |
|
| 13 |
-
ANNA_WITH_ADAM: Final = "anna_with_adam"
|
| 14 |
API: Final = "api"
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
GATEWAY: Final = "gateway"
|
| 21 |
LOCATION: Final = "location"
|
| 22 |
-
|
| 23 |
REBOOT: Final = "reboot"
|
| 24 |
SMILE: Final = "smile"
|
| 25 |
-
SMILE_OPEN_THERM: Final = "smile_open_therm"
|
| 26 |
-
SMILE_THERMO: Final = "smile_thermo"
|
| 27 |
STRETCH: Final = "stretch"
|
| 28 |
STRETCH_USERNAME: Final = "stretch"
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
|
|
|
| 31 |
PLATFORMS: Final[list[str]] = [
|
| 32 |
Platform.BINARY_SENSOR,
|
| 33 |
Platform.BUTTON,
|
|
@@ -36,7 +166,20 @@
|
|
| 36 |
Platform.SELECT,
|
| 37 |
Platform.SENSOR,
|
| 38 |
Platform.SWITCH,
|
|
|
|
| 39 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
ZEROCONF_MAP: Final[dict[str, str]] = {
|
| 41 |
"smile": "Smile P1",
|
| 42 |
"smile_thermo": "Smile Anna",
|
|
@@ -55,7 +198,7 @@
|
|
| 55 |
"select_gateway_mode",
|
| 56 |
"select_regulation_mode",
|
| 57 |
"select_schedule",
|
| 58 |
-
"select_zone_profile"
|
| 59 |
]
|
| 60 |
type SelectOptionsType = Literal[
|
| 61 |
"available_schedules",
|
|
@@ -64,25 +207,3 @@
|
|
| 64 |
"regulation_modes",
|
| 65 |
"zone_profiles",
|
| 66 |
]
|
| 67 |
-
|
| 68 |
-
# Default directives
|
| 69 |
-
DEFAULT_MAX_TEMP: Final = 30
|
| 70 |
-
DEFAULT_MIN_TEMP: Final = 4
|
| 71 |
-
DEFAULT_PORT: Final = 80
|
| 72 |
-
DEFAULT_UPDATE_INTERVAL = timedelta(seconds=60)
|
| 73 |
-
DEFAULT_USERNAME: Final = "smile"
|
| 74 |
-
P1_UPDATE_INTERVAL = timedelta(seconds=10)
|
| 75 |
-
|
| 76 |
-
MASTER_THERMOSTATS: Final[list[str]] = [
|
| 77 |
-
"thermostat",
|
| 78 |
-
"thermostatic_radiator_valve",
|
| 79 |
-
"zone_thermometer",
|
| 80 |
-
"zone_thermostat",
|
| 81 |
-
]
|
| 82 |
-
|
| 83 |
-
# Select constants
|
| 84 |
-
SELECT_DHW_MODE: Final = "select_dhw_mode"
|
| 85 |
-
SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
|
| 86 |
-
SELECT_REGULATION_MODE: Final = "select_regulation_mode"
|
| 87 |
-
SELECT_SCHEDULE: Final = "select_schedule"
|
| 88 |
-
SELECT_ZONE_PROFILE: Final = "select_zone_profile"
|
|
|
|
| 6 |
|
| 7 |
from homeassistant.const import Platform
|
| 8 |
|
| 9 |
+
# Upstream basically the whole file excluding pw-beta options
|
| 10 |
+
|
| 11 |
DOMAIN: Final = "plugwise"
|
| 12 |
|
| 13 |
LOGGER = logging.getLogger(__package__)
|
| 14 |
|
|
|
|
| 15 |
API: Final = "api"
|
| 16 |
+
COORDINATOR: Final = "coordinator"
|
| 17 |
+
CONFIG_ENTRY: Final = "config_entry" # pw-beta service
|
| 18 |
+
CONF_HOMEKIT_EMULATION: Final = "homekit_emulation" # pw-beta options
|
| 19 |
+
CONF_REFRESH_INTERVAL: Final = "refresh_interval" # pw-beta options
|
| 20 |
+
CONF_MANUAL_PATH: Final = "Enter Manually"
|
| 21 |
GATEWAY: Final = "gateway"
|
| 22 |
LOCATION: Final = "location"
|
| 23 |
+
MAC_ADDRESS: Final = "mac_address"
|
| 24 |
REBOOT: Final = "reboot"
|
| 25 |
SMILE: Final = "smile"
|
|
|
|
|
|
|
| 26 |
STRETCH: Final = "stretch"
|
| 27 |
STRETCH_USERNAME: Final = "stretch"
|
| 28 |
+
SWITCH_GROUPS: Final[tuple[str, str]] = ("report", "switching")
|
| 29 |
+
UNIQUE_IDS: Final = "unique_ids"
|
| 30 |
+
ZIGBEE_MAC_ADDRESS: Final = "zigbee_mac_address"
|
| 31 |
+
|
| 32 |
+
# Binary Sensor constants
|
| 33 |
+
BINARY_SENSORS: Final = "binary_sensors"
|
| 34 |
+
BATTERY_STATE: Final = "low_battery"
|
| 35 |
+
COMPRESSOR_STATE: Final = "compressor_state"
|
| 36 |
+
COOLING_ENABLED: Final = "cooling_enabled"
|
| 37 |
+
COOLING_STATE: Final = "cooling_state"
|
| 38 |
+
DHW_STATE: Final = "dhw_state"
|
| 39 |
+
FLAME_STATE: Final = "flame_state"
|
| 40 |
+
HEATING_STATE: Final = "heating_state"
|
| 41 |
+
PLUGWISE_NOTIFICATION: Final = "plugwise_notification"
|
| 42 |
+
SECONDARY_BOILER_STATE: Final = "secondary_boiler_state"
|
| 43 |
+
|
| 44 |
+
# Climate constants
|
| 45 |
+
ACTIVE_PRESET: Final = "active_preset"
|
| 46 |
+
CLIMATE_MODE: Final = "climate_mode"
|
| 47 |
+
CONTROL_STATE: Final = "control_state"
|
| 48 |
+
COOLING_PRESENT: Final ="cooling_present"
|
| 49 |
+
DEV_CLASS: Final = "dev_class"
|
| 50 |
+
NONE : Final = "None"
|
| 51 |
+
TARGET_TEMP: Final = "setpoint"
|
| 52 |
+
TARGET_TEMP_HIGH: Final = "setpoint_high"
|
| 53 |
+
TARGET_TEMP_LOW: Final = "setpoint_low"
|
| 54 |
+
THERMOSTAT: Final = "thermostat"
|
| 55 |
+
|
| 56 |
+
# Config_flow constants
|
| 57 |
+
ANNA_WITH_ADAM: Final = "anna_with_adam"
|
| 58 |
+
CONTEXT: Final = "context"
|
| 59 |
+
FLOW_ID: Final = "flow_id"
|
| 60 |
+
FLOW_NET: Final = "Network: Smile/Stretch"
|
| 61 |
+
FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
|
| 62 |
+
FLOW_STRETCH: Final = "Stretch (Stretch)"
|
| 63 |
+
FLOW_TYPE: Final = "flow_type"
|
| 64 |
+
INIT: Final = "init"
|
| 65 |
+
PRODUCT: Final = "product"
|
| 66 |
+
SMILE_OPEN_THERM: Final = "smile_open_therm"
|
| 67 |
+
SMILE_THERMO: Final = "smile_thermo"
|
| 68 |
+
TITLE_PLACEHOLDERS: Final = "title_placeholders"
|
| 69 |
+
VERSION: Final = "version"
|
| 70 |
+
|
| 71 |
+
# Entity constants
|
| 72 |
+
AVAILABLE: Final = "available"
|
| 73 |
+
FIRMWARE: Final = "firmware"
|
| 74 |
+
HARDWARE: Final = "hardware"
|
| 75 |
+
MODEL: Final = "model"
|
| 76 |
+
MODEL_ID: Final = "model_id"
|
| 77 |
+
VENDOR: Final = "vendor"
|
| 78 |
+
|
| 79 |
+
# Number constants
|
| 80 |
+
MAX_BOILER_TEMP: Final = "maximum_boiler_temperature"
|
| 81 |
+
MAX_DHW_TEMP: Final = "max_dhw_temperature"
|
| 82 |
+
LOWER_BOUND: Final = "lower_bound"
|
| 83 |
+
RESOLUTION: Final = "resolution"
|
| 84 |
+
TEMPERATURE_OFFSET: Final = "temperature_offset"
|
| 85 |
+
UPPER_BOUND: Final = "upper_bound"
|
| 86 |
+
|
| 87 |
+
# Sensor constants
|
| 88 |
+
DHW_TEMP: Final = "dhw_temperature"
|
| 89 |
+
DHW_SETPOINT: Final = "domestic_hot_water_setpoint"
|
| 90 |
+
EL_CONSUMED: Final = "electricity_consumed"
|
| 91 |
+
EL_CONS_INTERVAL: Final = "electricity_consumed_interval"
|
| 92 |
+
EL_CONS_OP_CUMULATIVE: Final = "electricity_consumed_off_peak_cumulative"
|
| 93 |
+
EL_CONS_OP_INTERVAL: Final = "electricity_consumed_off_peak_interval"
|
| 94 |
+
EL_CONS_OP_POINT: Final = "electricity_consumed_off_peak_point"
|
| 95 |
+
EL_CONS_P_CUMULATIVE: Final = "electricity_consumed_peak_cumulative"
|
| 96 |
+
EL_CONS_P_INTERVAL: Final = "electricity_consumed_peak_interval"
|
| 97 |
+
EL_CONS_P_POINT: Final = "electricity_consumed_peak_point"
|
| 98 |
+
EL_CONS_POINT: Final = "electricity_consumed_point"
|
| 99 |
+
EL_PH1_CONSUMED: Final = "electricity_phase_one_consumed"
|
| 100 |
+
EL_PH2_CONSUMED: Final = "electricity_phase_two_consumed"
|
| 101 |
+
EL_PH3_CONSUMED: Final = "electricity_phase_three_consumed"
|
| 102 |
+
EL_PH1_PRODUCED: Final = "electricity_phase_one_produced"
|
| 103 |
+
EL_PH2_PRODUCED: Final = "electricity_phase_two_produced"
|
| 104 |
+
EL_PH3_PRODUCED: Final = "electricity_phase_three_produced"
|
| 105 |
+
EL_PRODUCED: Final = "electricity_produced"
|
| 106 |
+
EL_PROD_INTERVAL: Final = "electricity_produced_interval"
|
| 107 |
+
EL_PROD_OP_CUMULATIVE: Final = "electricity_produced_off_peak_cumulative"
|
| 108 |
+
EL_PROD_OP_INTERVAL: Final = "electricity_produced_off_peak_interval"
|
| 109 |
+
EL_PROD_OP_POINT: Final = "electricity_produced_off_peak_point"
|
| 110 |
+
EL_PROD_P_CUMULATIVE: Final = "electricity_produced_peak_cumulative"
|
| 111 |
+
EL_PROD_P_INTERVAL: Final = "electricity_produced_peak_interval"
|
| 112 |
+
EL_PROD_P_POINT: Final = "electricity_produced_peak_point"
|
| 113 |
+
EL_PROD_POINT: Final = "electricity_produced_point"
|
| 114 |
+
GAS_CONS_CUMULATIVE: Final = "gas_consumed_cumulative"
|
| 115 |
+
GAS_CONS_INTERVAL: Final = "gas_consumed_interval"
|
| 116 |
+
INTENDED_BOILER_TEMP: Final = "intended_boiler_temperature"
|
| 117 |
+
MOD_LEVEL: Final = "modulation_level"
|
| 118 |
+
NET_EL_POINT: Final = "net_electricity_point"
|
| 119 |
+
NET_EL_CUMULATIVE: Final = "net_electricity_cumulative"
|
| 120 |
+
OUTDOOR_AIR_TEMP: Final = "outdoor_air_temperature"
|
| 121 |
+
OUTDOOR_TEMP: Final = "outdoor_temperature"
|
| 122 |
+
RETURN_TEMP: Final = "return_temperature"
|
| 123 |
+
SENSORS: Final = "sensors"
|
| 124 |
+
TEMP_DIFF: Final = "temperature_difference"
|
| 125 |
+
VALVE_POS: Final = "valve_position"
|
| 126 |
+
VOLTAGE_PH1: Final = "voltage_phase_one"
|
| 127 |
+
VOLTAGE_PH2: Final = "voltage_phase_two"
|
| 128 |
+
VOLTAGE_PH3: Final = "voltage_phase_three"
|
| 129 |
+
WATER_TEMP: Final = "water_temperature"
|
| 130 |
+
WATER_PRESSURE: Final = "water_pressure"
|
| 131 |
+
|
| 132 |
+
# Select constants
|
| 133 |
+
AVAILABLE_SCHEDULES: Final = "available_schedules"
|
| 134 |
+
DHW_MODE: Final = "dhw_mode"
|
| 135 |
+
DHW_MODES: Final = "dhw_modes"
|
| 136 |
+
GATEWAY_MODES: Final = "gateway_modes"
|
| 137 |
+
REGULATION_MODES: Final = "regulation_modes"
|
| 138 |
+
ZONE_PROFILES: Final = "zone_profiles"
|
| 139 |
+
SELECT_DHW_MODE: Final = "select_dhw_mode"
|
| 140 |
+
SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
|
| 141 |
+
SELECT_REGULATION_MODE: Final = "select_regulation_mode"
|
| 142 |
+
SELECT_SCHEDULE: Final = "select_schedule"
|
| 143 |
+
SELECT_ZONE_PROFILE: Final = "select_zone_profile"
|
| 144 |
+
|
| 145 |
+
# Switch constants
|
| 146 |
+
DHW_CM_SWITCH: Final = "dhw_cm_switch"
|
| 147 |
+
LOCK: Final = "lock"
|
| 148 |
+
MEMBERS: Final ="members"
|
| 149 |
+
RELAY: Final = "relay"
|
| 150 |
+
COOLING_ENA_SWITCH: Final ="cooling_ena_switch"
|
| 151 |
+
SWITCHES: Final = "switches"
|
| 152 |
+
|
| 153 |
+
# Default directives
|
| 154 |
+
DEFAULT_PORT: Final[int] = 80
|
| 155 |
+
DEFAULT_TIMEOUT: Final[int] = 30
|
| 156 |
+
DEFAULT_UPDATE_INTERVAL: Final = timedelta(seconds=60)
|
| 157 |
+
DEFAULT_USERNAME: Final = "smile"
|
| 158 |
+
P1_UPDATE_INTERVAL: Final = timedelta(seconds=10)
|
| 159 |
|
| 160 |
+
# --- Const for Plugwise Smile and Stretch
|
| 161 |
PLATFORMS: Final[list[str]] = [
|
| 162 |
Platform.BINARY_SENSOR,
|
| 163 |
Platform.BUTTON,
|
|
|
|
| 166 |
Platform.SELECT,
|
| 167 |
Platform.SENSOR,
|
| 168 |
Platform.SWITCH,
|
| 169 |
+
Platform.WATER_HEATER,
|
| 170 |
]
|
| 171 |
+
SERVICE_DELETE: Final = "delete_notification"
|
| 172 |
+
SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
|
| 173 |
+
|
| 174 |
+
# Climate const:
|
| 175 |
+
MASTER_THERMOSTATS: Final[list[str]] = [
|
| 176 |
+
"thermostat",
|
| 177 |
+
"zone_thermometer",
|
| 178 |
+
"zone_thermostat",
|
| 179 |
+
"thermostatic_radiator_valve",
|
| 180 |
+
]
|
| 181 |
+
|
| 182 |
+
# Config_flow const:
|
| 183 |
ZEROCONF_MAP: Final[dict[str, str]] = {
|
| 184 |
"smile": "Smile P1",
|
| 185 |
"smile_thermo": "Smile Anna",
|
|
|
|
| 198 |
"select_gateway_mode",
|
| 199 |
"select_regulation_mode",
|
| 200 |
"select_schedule",
|
| 201 |
+
"select_zone_profile"
|
| 202 |
]
|
| 203 |
type SelectOptionsType = Literal[
|
| 204 |
"available_schedules",
|
|
|
|
| 207 |
"regulation_modes",
|
| 208 |
"zone_profiles",
|
| 209 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,8 +1,7 @@
|
|
| 1 |
"""DataUpdateCoordinator for Plugwise."""
|
| 2 |
|
| 3 |
-
from
|
| 4 |
|
| 5 |
-
from packaging.version import Version
|
| 6 |
from plugwise import GwEntityData, Smile
|
| 7 |
from plugwise.exceptions import (
|
| 8 |
ConnectionFailedError,
|
|
@@ -15,21 +14,29 @@
|
|
| 15 |
)
|
| 16 |
|
| 17 |
from homeassistant.config_entries import ConfigEntry
|
| 18 |
-
from homeassistant.const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
from homeassistant.core import HomeAssistant
|
| 20 |
from homeassistant.exceptions import ConfigEntryError
|
| 21 |
from homeassistant.helpers import device_registry as dr
|
| 22 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
| 23 |
from homeassistant.helpers.debounce import Debouncer
|
| 24 |
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
|
|
| 25 |
|
| 26 |
from .const import (
|
| 27 |
-
DEFAULT_PORT,
|
| 28 |
DEFAULT_UPDATE_INTERVAL,
|
| 29 |
-
|
| 30 |
DOMAIN,
|
|
|
|
| 31 |
LOGGER,
|
| 32 |
P1_UPDATE_INTERVAL,
|
|
|
|
| 33 |
)
|
| 34 |
|
| 35 |
type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
|
|
@@ -40,29 +47,36 @@
|
|
| 40 |
|
| 41 |
config_entry: PlugwiseConfigEntry
|
| 42 |
|
| 43 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
"""Initialize the coordinator."""
|
| 45 |
super().__init__(
|
| 46 |
hass,
|
| 47 |
LOGGER,
|
| 48 |
config_entry=config_entry,
|
| 49 |
name=DOMAIN,
|
|
|
|
|
|
|
| 50 |
update_interval=DEFAULT_UPDATE_INTERVAL,
|
| 51 |
# Don't refresh immediately, give the device time to process
|
| 52 |
# the change in state before we query it.
|
| 53 |
request_refresh_debouncer=Debouncer(
|
| 54 |
hass,
|
| 55 |
LOGGER,
|
| 56 |
-
cooldown=
|
| 57 |
immediate=False,
|
| 58 |
),
|
| 59 |
)
|
| 60 |
|
| 61 |
self.api = Smile(
|
| 62 |
host=self.config_entry.data[CONF_HOST],
|
| 63 |
-
username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
|
| 64 |
password=self.config_entry.data[CONF_PASSWORD],
|
| 65 |
-
port=self.config_entry.data
|
|
|
|
| 66 |
websession=async_get_clientsession(hass, verify_ssl=False),
|
| 67 |
)
|
| 68 |
self._connected: bool = False
|
|
@@ -78,10 +92,16 @@
|
|
| 78 |
"""
|
| 79 |
version = await self.api.connect()
|
| 80 |
self._connected = isinstance(version, Version)
|
| 81 |
-
if self._connected
|
| 82 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
@override
|
| 85 |
async def _async_setup(self) -> None:
|
| 86 |
"""Initialize the update_data process."""
|
| 87 |
device_reg = dr.async_get(self.hass)
|
|
@@ -95,7 +115,6 @@
|
|
| 95 |
if identifier[0] == DOMAIN
|
| 96 |
}
|
| 97 |
|
| 98 |
-
@override
|
| 99 |
async def _async_update_data(self) -> dict[str, GwEntityData]:
|
| 100 |
"""Fetch data from Plugwise."""
|
| 101 |
try:
|
|
@@ -133,25 +152,27 @@
|
|
| 133 |
translation_key="unsupported_firmware",
|
| 134 |
) from err
|
| 135 |
|
|
|
|
| 136 |
self._add_remove_devices(data)
|
| 137 |
self._update_device_firmware(data)
|
| 138 |
return data
|
| 139 |
|
| 140 |
def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
|
| 141 |
"""Add new Plugwise devices, remove non-existing devices."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
set_of_data = set(data)
|
| 143 |
-
# Check for new or removed devices,
|
| 144 |
-
# 'new_devices' contains all devices present in 'data'
|
| 145 |
-
# at init ('self._current_devices' is empty) this is
|
| 146 |
-
# required for the proper initialization of all the
|
| 147 |
-
# present platform entities.
|
| 148 |
self.new_devices = set_of_data - self._current_devices
|
| 149 |
for device_id in self.new_devices:
|
| 150 |
-
self._firmware_list.setdefault(device_id, data[device_id].get(
|
| 151 |
|
| 152 |
-
current_devices =
|
| 153 |
-
self._stored_devices if not self._current_devices else self._current_devices
|
| 154 |
-
)
|
| 155 |
self._current_devices = set_of_data
|
| 156 |
if removed_devices := (current_devices - set_of_data): # device(s) to remove
|
| 157 |
self._remove_devices(removed_devices)
|
|
@@ -160,9 +181,7 @@
|
|
| 160 |
"""Clean registries when removed devices found."""
|
| 161 |
device_reg = dr.async_get(self.hass)
|
| 162 |
for device_id in removed_devices:
|
| 163 |
-
if (
|
| 164 |
-
device_entry := device_reg.async_get_device({(DOMAIN, device_id)})
|
| 165 |
-
) is not None:
|
| 166 |
device_reg.async_update_device(
|
| 167 |
device_entry.id, remove_config_entry_id=self.config_entry.entry_id
|
| 168 |
)
|
|
@@ -175,14 +194,15 @@
|
|
| 175 |
|
| 176 |
self._firmware_list.pop(device_id, None)
|
| 177 |
|
|
|
|
| 178 |
def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
|
| 179 |
"""Detect firmware changes and update the device registry."""
|
| 180 |
for device_id, device in data.items():
|
| 181 |
# Only update firmware when the key is present and not None, to avoid
|
| 182 |
# wiping stored firmware on partial or transient updates.
|
| 183 |
-
if
|
| 184 |
continue
|
| 185 |
-
new_firmware = device.get(
|
| 186 |
if new_firmware is None:
|
| 187 |
continue
|
| 188 |
if (
|
|
@@ -196,9 +216,7 @@
|
|
| 196 |
def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> bool:
|
| 197 |
"""Update device sw_version in device_registry."""
|
| 198 |
device_reg = dr.async_get(self.hass)
|
| 199 |
-
if (
|
| 200 |
-
device_entry := device_reg.async_get_device({(DOMAIN, device_id)})
|
| 201 |
-
) is not None:
|
| 202 |
device_reg.async_update_device(device_entry.id, sw_version=firmware)
|
| 203 |
LOGGER.debug(
|
| 204 |
"Firmware in device_registry updated for %s %s %s",
|
|
|
|
| 1 |
"""DataUpdateCoordinator for Plugwise."""
|
| 2 |
|
| 3 |
+
from datetime import timedelta
|
| 4 |
|
|
|
|
| 5 |
from plugwise import GwEntityData, Smile
|
| 6 |
from plugwise.exceptions import (
|
| 7 |
ConnectionFailedError,
|
|
|
|
| 14 |
)
|
| 15 |
|
| 16 |
from homeassistant.config_entries import ConfigEntry
|
| 17 |
+
from homeassistant.const import (
|
| 18 |
+
CONF_HOST,
|
| 19 |
+
CONF_PASSWORD,
|
| 20 |
+
CONF_PORT,
|
| 21 |
+
CONF_SCAN_INTERVAL, # pw-beta options
|
| 22 |
+
CONF_USERNAME,
|
| 23 |
+
)
|
| 24 |
from homeassistant.core import HomeAssistant
|
| 25 |
from homeassistant.exceptions import ConfigEntryError
|
| 26 |
from homeassistant.helpers import device_registry as dr
|
| 27 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
| 28 |
from homeassistant.helpers.debounce import Debouncer
|
| 29 |
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
| 30 |
+
from packaging.version import Version
|
| 31 |
|
| 32 |
from .const import (
|
|
|
|
| 33 |
DEFAULT_UPDATE_INTERVAL,
|
| 34 |
+
DEV_CLASS,
|
| 35 |
DOMAIN,
|
| 36 |
+
FIRMWARE,
|
| 37 |
LOGGER,
|
| 38 |
P1_UPDATE_INTERVAL,
|
| 39 |
+
SWITCH_GROUPS,
|
| 40 |
)
|
| 41 |
|
| 42 |
type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
|
|
|
|
| 47 |
|
| 48 |
config_entry: PlugwiseConfigEntry
|
| 49 |
|
| 50 |
+
def __init__(
|
| 51 |
+
self,
|
| 52 |
+
hass: HomeAssistant,
|
| 53 |
+
cooldown: float,
|
| 54 |
+
config_entry: PlugwiseConfigEntry,
|
| 55 |
+
) -> None: # pw-beta cooldown
|
| 56 |
"""Initialize the coordinator."""
|
| 57 |
super().__init__(
|
| 58 |
hass,
|
| 59 |
LOGGER,
|
| 60 |
config_entry=config_entry,
|
| 61 |
name=DOMAIN,
|
| 62 |
+
# Core directly updates from const's DEFAULT_SCAN_INTERVAL
|
| 63 |
+
# Upstream check correct progress for adjusting
|
| 64 |
update_interval=DEFAULT_UPDATE_INTERVAL,
|
| 65 |
# Don't refresh immediately, give the device time to process
|
| 66 |
# the change in state before we query it.
|
| 67 |
request_refresh_debouncer=Debouncer(
|
| 68 |
hass,
|
| 69 |
LOGGER,
|
| 70 |
+
cooldown=cooldown,
|
| 71 |
immediate=False,
|
| 72 |
),
|
| 73 |
)
|
| 74 |
|
| 75 |
self.api = Smile(
|
| 76 |
host=self.config_entry.data[CONF_HOST],
|
|
|
|
| 77 |
password=self.config_entry.data[CONF_PASSWORD],
|
| 78 |
+
port=self.config_entry.data[CONF_PORT],
|
| 79 |
+
username=self.config_entry.data[CONF_USERNAME],
|
| 80 |
websession=async_get_clientsession(hass, verify_ssl=False),
|
| 81 |
)
|
| 82 |
self._connected: bool = False
|
|
|
|
| 92 |
"""
|
| 93 |
version = await self.api.connect()
|
| 94 |
self._connected = isinstance(version, Version)
|
| 95 |
+
if self._connected:
|
| 96 |
+
if self.api.smile.type == "power":
|
| 97 |
+
self.update_interval = P1_UPDATE_INTERVAL
|
| 98 |
+
if (custom_time := self.config_entry.options.get(CONF_SCAN_INTERVAL)) is not None:
|
| 99 |
+
self.update_interval = timedelta(
|
| 100 |
+
seconds=int(custom_time)
|
| 101 |
+
) # pragma: no cover # pw-beta options
|
| 102 |
+
|
| 103 |
+
LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
|
| 104 |
|
|
|
|
| 105 |
async def _async_setup(self) -> None:
|
| 106 |
"""Initialize the update_data process."""
|
| 107 |
device_reg = dr.async_get(self.hass)
|
|
|
|
| 115 |
if identifier[0] == DOMAIN
|
| 116 |
}
|
| 117 |
|
|
|
|
| 118 |
async def _async_update_data(self) -> dict[str, GwEntityData]:
|
| 119 |
"""Fetch data from Plugwise."""
|
| 120 |
try:
|
|
|
|
| 152 |
translation_key="unsupported_firmware",
|
| 153 |
) from err
|
| 154 |
|
| 155 |
+
LOGGER.debug("%s data: %s", self.api.smile.name, data)
|
| 156 |
self._add_remove_devices(data)
|
| 157 |
self._update_device_firmware(data)
|
| 158 |
return data
|
| 159 |
|
| 160 |
def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
|
| 161 |
"""Add new Plugwise devices, remove non-existing devices."""
|
| 162 |
+
# Block switch-groups, use HA group helper instead to create switch-groups
|
| 163 |
+
for device_id, device in data.copy().items():
|
| 164 |
+
if device.get(DEV_CLASS) in SWITCH_GROUPS:
|
| 165 |
+
data.pop(device_id)
|
| 166 |
+
|
| 167 |
+
# Collect new or removed devices,
|
| 168 |
+
# 'new_devices' contains all devices present in 'data' at init ('self._current_devices' is empty)
|
| 169 |
+
# this is required for the initialization of the available platform entities.
|
| 170 |
set_of_data = set(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
self.new_devices = set_of_data - self._current_devices
|
| 172 |
for device_id in self.new_devices:
|
| 173 |
+
self._firmware_list.setdefault(device_id, data[device_id].get(FIRMWARE))
|
| 174 |
|
| 175 |
+
current_devices = self._stored_devices if not self._current_devices else self._current_devices
|
|
|
|
|
|
|
| 176 |
self._current_devices = set_of_data
|
| 177 |
if removed_devices := (current_devices - set_of_data): # device(s) to remove
|
| 178 |
self._remove_devices(removed_devices)
|
|
|
|
| 181 |
"""Clean registries when removed devices found."""
|
| 182 |
device_reg = dr.async_get(self.hass)
|
| 183 |
for device_id in removed_devices:
|
| 184 |
+
if (device_entry := device_reg.async_get_device({(DOMAIN, device_id)})) is not None:
|
|
|
|
|
|
|
| 185 |
device_reg.async_update_device(
|
| 186 |
device_entry.id, remove_config_entry_id=self.config_entry.entry_id
|
| 187 |
)
|
|
|
|
| 194 |
|
| 195 |
self._firmware_list.pop(device_id, None)
|
| 196 |
|
| 197 |
+
|
| 198 |
def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
|
| 199 |
"""Detect firmware changes and update the device registry."""
|
| 200 |
for device_id, device in data.items():
|
| 201 |
# Only update firmware when the key is present and not None, to avoid
|
| 202 |
# wiping stored firmware on partial or transient updates.
|
| 203 |
+
if FIRMWARE not in device:
|
| 204 |
continue
|
| 205 |
+
new_firmware = device.get(FIRMWARE)
|
| 206 |
if new_firmware is None:
|
| 207 |
continue
|
| 208 |
if (
|
|
|
|
| 216 |
def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> bool:
|
| 217 |
"""Update device sw_version in device_registry."""
|
| 218 |
device_reg = dr.async_get(self.hass)
|
| 219 |
+
if (device_entry := device_reg.async_get_device({(DOMAIN, device_id)})) is not None:
|
|
|
|
|
|
|
| 220 |
device_reg.async_update_device(device_entry.id, sw_version=firmware)
|
| 221 |
LOGGER.debug(
|
| 222 |
"Firmware in device_registry updated for %s %s %s",
|
|
@@ -1,8 +1,6 @@
|
|
| 1 |
"""Generic Plugwise Entity Class."""
|
| 2 |
|
| 3 |
-
from
|
| 4 |
-
|
| 5 |
-
from plugwise import GwEntityData
|
| 6 |
|
| 7 |
from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
|
| 8 |
from homeassistant.helpers.device_registry import (
|
|
@@ -12,7 +10,19 @@
|
|
| 12 |
)
|
| 13 |
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
| 14 |
|
| 15 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 17 |
|
| 18 |
|
|
@@ -43,9 +53,9 @@
|
|
| 43 |
|
| 44 |
# Build connections set
|
| 45 |
connections = set()
|
| 46 |
-
if mac := self.device.get(
|
| 47 |
connections.add((CONNECTION_NETWORK_MAC, mac))
|
| 48 |
-
if zigbee_mac := self.device.get(
|
| 49 |
connections.add((CONNECTION_ZIGBEE, zigbee_mac))
|
| 50 |
|
| 51 |
# Set base device info
|
|
@@ -53,12 +63,12 @@
|
|
| 53 |
configuration_url=configuration_url,
|
| 54 |
identifiers={(DOMAIN, device_id)},
|
| 55 |
connections=connections,
|
| 56 |
-
manufacturer=self.device.get(
|
| 57 |
-
model=self.device.get(
|
| 58 |
-
model_id=self.device.get(
|
| 59 |
name=api.smile.name,
|
| 60 |
-
sw_version=self.device.get(
|
| 61 |
-
hw_version=self.device.get(
|
| 62 |
)
|
| 63 |
|
| 64 |
# Add extra info if not the gateway device
|
|
@@ -71,12 +81,13 @@
|
|
| 71 |
)
|
| 72 |
|
| 73 |
@property
|
| 74 |
-
@override
|
| 75 |
def available(self) -> bool:
|
| 76 |
"""Return if entity is available."""
|
| 77 |
return (
|
|
|
|
|
|
|
| 78 |
self._dev_id in self.coordinator.data
|
| 79 |
-
and (
|
| 80 |
and super().available
|
| 81 |
)
|
| 82 |
|
|
|
|
| 1 |
"""Generic Plugwise Entity Class."""
|
| 2 |
|
| 3 |
+
from plugwise.constants import GwEntityData
|
|
|
|
|
|
|
| 4 |
|
| 5 |
from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
|
| 6 |
from homeassistant.helpers.device_registry import (
|
|
|
|
| 10 |
)
|
| 11 |
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
| 12 |
|
| 13 |
+
from .const import (
|
| 14 |
+
AVAILABLE,
|
| 15 |
+
DOMAIN,
|
| 16 |
+
FIRMWARE,
|
| 17 |
+
HARDWARE,
|
| 18 |
+
MAC_ADDRESS,
|
| 19 |
+
MODEL,
|
| 20 |
+
MODEL_ID,
|
| 21 |
+
VENDOR,
|
| 22 |
+
ZIGBEE_MAC_ADDRESS,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Upstream consts
|
| 26 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 27 |
|
| 28 |
|
|
|
|
| 53 |
|
| 54 |
# Build connections set
|
| 55 |
connections = set()
|
| 56 |
+
if mac := self.device.get(MAC_ADDRESS):
|
| 57 |
connections.add((CONNECTION_NETWORK_MAC, mac))
|
| 58 |
+
if zigbee_mac := self.device.get(ZIGBEE_MAC_ADDRESS):
|
| 59 |
connections.add((CONNECTION_ZIGBEE, zigbee_mac))
|
| 60 |
|
| 61 |
# Set base device info
|
|
|
|
| 63 |
configuration_url=configuration_url,
|
| 64 |
identifiers={(DOMAIN, device_id)},
|
| 65 |
connections=connections,
|
| 66 |
+
manufacturer=self.device.get(VENDOR),
|
| 67 |
+
model=self.device.get(MODEL),
|
| 68 |
+
model_id=self.device.get(MODEL_ID),
|
| 69 |
name=api.smile.name,
|
| 70 |
+
sw_version=self.device.get(FIRMWARE),
|
| 71 |
+
hw_version=self.device.get(HARDWARE),
|
| 72 |
)
|
| 73 |
|
| 74 |
# Add extra info if not the gateway device
|
|
|
|
| 81 |
)
|
| 82 |
|
| 83 |
@property
|
|
|
|
| 84 |
def available(self) -> bool:
|
| 85 |
"""Return if entity is available."""
|
| 86 |
return (
|
| 87 |
+
# Upstream: Do not change the AVAILABLE line below: some Plugwise devices and zones
|
| 88 |
+
# Upstream: do not provide their availability-status!
|
| 89 |
self._dev_id in self.coordinator.data
|
| 90 |
+
and (self.device.get(AVAILABLE, True) is True)
|
| 91 |
and super().available
|
| 92 |
)
|
| 93 |
|
|
@@ -120,5 +120,8 @@
|
|
| 120 |
"default": "mdi:lock"
|
| 121 |
}
|
| 122 |
}
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
}
|
|
|
|
| 120 |
"default": "mdi:lock"
|
| 121 |
}
|
| 122 |
}
|
| 123 |
+
},
|
| 124 |
+
"services": {
|
| 125 |
+
"delete_notification": "mdi:trash-can"
|
| 126 |
}
|
| 127 |
}
|
|
@@ -1,13 +1,13 @@
|
|
| 1 |
{
|
| 2 |
"domain": "plugwise",
|
| 3 |
-
"name": "Plugwise",
|
| 4 |
"codeowners": ["@CoMPaTech", "@bouwew"],
|
| 5 |
"config_flow": true,
|
| 6 |
-
"documentation": "https://
|
| 7 |
"integration_type": "hub",
|
| 8 |
"iot_class": "local_polling",
|
| 9 |
"loggers": ["plugwise"],
|
| 10 |
-
"
|
| 11 |
-
"
|
| 12 |
"zeroconf": ["_plugwise._tcp.local."]
|
| 13 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"domain": "plugwise",
|
| 3 |
+
"name": "Plugwise Beta",
|
| 4 |
"codeowners": ["@CoMPaTech", "@bouwew"],
|
| 5 |
"config_flow": true,
|
| 6 |
+
"documentation": "https://github.com/plugwise/plugwise-beta",
|
| 7 |
"integration_type": "hub",
|
| 8 |
"iot_class": "local_polling",
|
| 9 |
"loggers": ["plugwise"],
|
| 10 |
+
"requirements": ["plugwise==1.12.0"],
|
| 11 |
+
"version": "0.65.0",
|
| 12 |
"zeroconf": ["_plugwise._tcp.local."]
|
| 13 |
}
|
|
@@ -1,7 +1,6 @@
|
|
| 1 |
"""Number platform for Plugwise integration."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
-
from typing import override
|
| 5 |
|
| 6 |
from homeassistant.components.number import (
|
| 7 |
NumberDeviceClass,
|
|
@@ -13,7 +12,17 @@
|
|
| 13 |
from homeassistant.core import HomeAssistant, callback
|
| 14 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 15 |
|
| 16 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 18 |
from .entity import PlugwiseEntity
|
| 19 |
from .util import plugwise_command
|
|
@@ -28,24 +37,18 @@
|
|
| 28 |
key: NumberType
|
| 29 |
|
| 30 |
|
|
|
|
| 31 |
NUMBER_TYPES = (
|
| 32 |
PlugwiseNumberEntityDescription(
|
| 33 |
-
key=
|
| 34 |
-
translation_key=
|
| 35 |
-
device_class=NumberDeviceClass.TEMPERATURE,
|
| 36 |
-
entity_category=EntityCategory.CONFIG,
|
| 37 |
-
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 38 |
-
),
|
| 39 |
-
PlugwiseNumberEntityDescription(
|
| 40 |
-
key="max_dhw_temperature",
|
| 41 |
-
translation_key="max_dhw_temperature",
|
| 42 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 43 |
entity_category=EntityCategory.CONFIG,
|
| 44 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 45 |
),
|
| 46 |
PlugwiseNumberEntityDescription(
|
| 47 |
-
key=
|
| 48 |
-
translation_key=
|
| 49 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 50 |
entity_category=EntityCategory.CONFIG,
|
| 51 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
@@ -58,7 +61,8 @@
|
|
| 58 |
entry: PlugwiseConfigEntry,
|
| 59 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 60 |
) -> None:
|
| 61 |
-
"""Set up Plugwise number platform."""
|
|
|
|
| 62 |
coordinator = entry.runtime_data
|
| 63 |
|
| 64 |
@callback
|
|
@@ -67,12 +71,28 @@
|
|
| 67 |
if not coordinator.new_devices:
|
| 68 |
return
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
_add_entities()
|
| 78 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
@@ -91,28 +111,28 @@
|
|
| 91 |
) -> None:
|
| 92 |
"""Initiate Plugwise Number."""
|
| 93 |
super().__init__(coordinator, device_id)
|
| 94 |
-
self._attr_mode = NumberMode.BOX
|
| 95 |
-
self._attr_native_max_value = self.device[description.key]["upper_bound"]
|
| 96 |
-
self._attr_native_min_value = self.device[description.key]["lower_bound"]
|
| 97 |
-
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 98 |
self.device_id = device_id
|
| 99 |
self.entity_description = description
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
native_step =
|
| 102 |
-
if description.key !=
|
| 103 |
native_step = max(native_step, 0.5)
|
| 104 |
self._attr_native_step = native_step
|
| 105 |
|
| 106 |
@property
|
| 107 |
-
|
| 108 |
-
def native_value(self) -> float:
|
| 109 |
"""Return the present setpoint value."""
|
| 110 |
-
return self.device
|
| 111 |
|
| 112 |
@plugwise_command
|
| 113 |
-
@override
|
| 114 |
async def async_set_native_value(self, value: float) -> None:
|
| 115 |
"""Change to the new setpoint value."""
|
| 116 |
-
await self.coordinator.api.set_number(
|
| 117 |
-
|
|
|
|
| 118 |
)
|
|
|
|
| 1 |
"""Number platform for Plugwise integration."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
|
|
|
| 4 |
|
| 5 |
from homeassistant.components.number import (
|
| 6 |
NumberDeviceClass,
|
|
|
|
| 12 |
from homeassistant.core import HomeAssistant, callback
|
| 13 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 14 |
|
| 15 |
+
from .const import (
|
| 16 |
+
LOGGER,
|
| 17 |
+
LOWER_BOUND,
|
| 18 |
+
MAX_BOILER_TEMP,
|
| 19 |
+
RESOLUTION,
|
| 20 |
+
TEMPERATURE_OFFSET,
|
| 21 |
+
UPPER_BOUND,
|
| 22 |
+
NumberType,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Upstream consts
|
| 26 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 27 |
from .entity import PlugwiseEntity
|
| 28 |
from .util import plugwise_command
|
|
|
|
| 37 |
key: NumberType
|
| 38 |
|
| 39 |
|
| 40 |
+
# Upstream + is there a reason we didn't rename this one prefixed?
|
| 41 |
NUMBER_TYPES = (
|
| 42 |
PlugwiseNumberEntityDescription(
|
| 43 |
+
key=MAX_BOILER_TEMP,
|
| 44 |
+
translation_key=MAX_BOILER_TEMP,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 46 |
entity_category=EntityCategory.CONFIG,
|
| 47 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 48 |
),
|
| 49 |
PlugwiseNumberEntityDescription(
|
| 50 |
+
key=TEMPERATURE_OFFSET,
|
| 51 |
+
translation_key=TEMPERATURE_OFFSET,
|
| 52 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 53 |
entity_category=EntityCategory.CONFIG,
|
| 54 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
| 61 |
entry: PlugwiseConfigEntry,
|
| 62 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 63 |
) -> None:
|
| 64 |
+
"""Set up Plugwise number platform from a config entry."""
|
| 65 |
+
# Upstream above to adhere to standard used
|
| 66 |
coordinator = entry.runtime_data
|
| 67 |
|
| 68 |
@callback
|
|
|
|
| 71 |
if not coordinator.new_devices:
|
| 72 |
return
|
| 73 |
|
| 74 |
+
# Upstream consts
|
| 75 |
+
# async_add_entities(
|
| 76 |
+
# PlugwiseNumberEntity(coordinator, device_id, description)
|
| 77 |
+
# for device_id in coordinator.new_devices
|
| 78 |
+
# for description in NUMBER_TYPES
|
| 79 |
+
# if description.key in coordinator.data.devices[device_id]
|
| 80 |
+
# )
|
| 81 |
+
|
| 82 |
+
# pw-beta alternative for debugging
|
| 83 |
+
entities: list[PlugwiseNumberEntity] = []
|
| 84 |
+
for device_id in coordinator.new_devices:
|
| 85 |
+
device = coordinator.data[device_id]
|
| 86 |
+
for description in NUMBER_TYPES:
|
| 87 |
+
if description.key in device:
|
| 88 |
+
entities.append(
|
| 89 |
+
PlugwiseNumberEntity(coordinator, device_id, description)
|
| 90 |
+
)
|
| 91 |
+
LOGGER.debug(
|
| 92 |
+
"Add %s %s number", device["name"], description.translation_key
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
async_add_entities(entities)
|
| 96 |
|
| 97 |
_add_entities()
|
| 98 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
|
|
| 111 |
) -> None:
|
| 112 |
"""Initiate Plugwise Number."""
|
| 113 |
super().__init__(coordinator, device_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
self.device_id = device_id
|
| 115 |
self.entity_description = description
|
| 116 |
+
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 117 |
+
self._attr_mode = NumberMode.BOX
|
| 118 |
+
ctrl = self.device.get(description.key, {})
|
| 119 |
+
self._attr_native_max_value = ctrl.get(UPPER_BOUND, 100.0) # Upstream const
|
| 120 |
+
self._attr_native_min_value = ctrl.get(LOWER_BOUND, 0.0) # Upstream const
|
| 121 |
|
| 122 |
+
native_step = ctrl.get(RESOLUTION, 0.5) # Upstream const
|
| 123 |
+
if description.key != TEMPERATURE_OFFSET: # Upstream const
|
| 124 |
native_step = max(native_step, 0.5)
|
| 125 |
self._attr_native_step = native_step
|
| 126 |
|
| 127 |
@property
|
| 128 |
+
def native_value(self) -> float | None:
|
|
|
|
| 129 |
"""Return the present setpoint value."""
|
| 130 |
+
return self.device.get(self.entity_description.key, {}).get("setpoint")
|
| 131 |
|
| 132 |
@plugwise_command
|
|
|
|
| 133 |
async def async_set_native_value(self, value: float) -> None:
|
| 134 |
"""Change to the new setpoint value."""
|
| 135 |
+
await self.coordinator.api.set_number(self.device_id, self.entity_description.key, value)
|
| 136 |
+
LOGGER.debug(
|
| 137 |
+
"Setting %s to %s was successful", self.entity_description.key, value
|
| 138 |
)
|
|
@@ -1,7 +1,6 @@
|
|
| 1 |
"""Plugwise Select component for Home Assistant."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
-
from typing import override
|
| 5 |
|
| 6 |
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
| 7 |
from homeassistant.const import STATE_ON, EntityCategory
|
|
@@ -9,14 +8,23 @@
|
|
| 9 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 10 |
|
| 11 |
from .const import (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
SELECT_DHW_MODE,
|
| 13 |
SELECT_GATEWAY_MODE,
|
| 14 |
SELECT_REGULATION_MODE,
|
| 15 |
SELECT_SCHEDULE,
|
| 16 |
SELECT_ZONE_PROFILE,
|
|
|
|
| 17 |
SelectOptionsType,
|
| 18 |
SelectType,
|
| 19 |
)
|
|
|
|
|
|
|
| 20 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 21 |
from .entity import PlugwiseEntity
|
| 22 |
from .util import plugwise_command
|
|
@@ -32,35 +40,36 @@
|
|
| 32 |
options_key: SelectOptionsType
|
| 33 |
|
| 34 |
|
|
|
|
| 35 |
SELECT_TYPES = (
|
| 36 |
PlugwiseSelectEntityDescription(
|
| 37 |
key=SELECT_SCHEDULE,
|
| 38 |
translation_key=SELECT_SCHEDULE,
|
| 39 |
-
options_key=
|
| 40 |
),
|
| 41 |
PlugwiseSelectEntityDescription(
|
| 42 |
key=SELECT_REGULATION_MODE,
|
| 43 |
translation_key=SELECT_REGULATION_MODE,
|
| 44 |
entity_category=EntityCategory.CONFIG,
|
| 45 |
-
options_key=
|
| 46 |
),
|
| 47 |
PlugwiseSelectEntityDescription(
|
| 48 |
key=SELECT_DHW_MODE,
|
| 49 |
translation_key=SELECT_DHW_MODE,
|
| 50 |
entity_category=EntityCategory.CONFIG,
|
| 51 |
-
options_key=
|
| 52 |
),
|
| 53 |
PlugwiseSelectEntityDescription(
|
| 54 |
key=SELECT_GATEWAY_MODE,
|
| 55 |
translation_key=SELECT_GATEWAY_MODE,
|
| 56 |
entity_category=EntityCategory.CONFIG,
|
| 57 |
-
options_key=
|
| 58 |
),
|
| 59 |
PlugwiseSelectEntityDescription(
|
| 60 |
key=SELECT_ZONE_PROFILE,
|
| 61 |
translation_key=SELECT_ZONE_PROFILE,
|
| 62 |
entity_category=EntityCategory.CONFIG,
|
| 63 |
-
options_key=
|
| 64 |
),
|
| 65 |
)
|
| 66 |
|
|
@@ -70,7 +79,7 @@
|
|
| 70 |
entry: PlugwiseConfigEntry,
|
| 71 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 72 |
) -> None:
|
| 73 |
-
"""Set up
|
| 74 |
coordinator = entry.runtime_data
|
| 75 |
|
| 76 |
@callback
|
|
@@ -79,12 +88,27 @@
|
|
| 79 |
if not coordinator.new_devices:
|
| 80 |
return
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
_add_entities()
|
| 90 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
@@ -106,29 +130,36 @@
|
|
| 106 |
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
| 107 |
self.entity_description = entity_description
|
| 108 |
|
| 109 |
-
self.
|
| 110 |
-
if (
|
| 111 |
-
self.
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
@property
|
| 114 |
-
@override
|
| 115 |
def current_option(self) -> str | None:
|
| 116 |
"""Return the selected entity option to represent the entity state."""
|
| 117 |
-
return self.device
|
| 118 |
|
| 119 |
@property
|
| 120 |
-
@override
|
| 121 |
def options(self) -> list[str]:
|
| 122 |
"""Return the available select-options."""
|
| 123 |
-
return self.device
|
| 124 |
|
| 125 |
@plugwise_command
|
| 126 |
-
@override
|
| 127 |
async def async_select_option(self, option: str) -> None:
|
| 128 |
"""Change to the selected entity option.
|
| 129 |
|
| 130 |
-
|
|
|
|
|
|
|
| 131 |
"""
|
| 132 |
await self.coordinator.api.set_select(
|
| 133 |
-
self.entity_description.key, self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
)
|
|
|
|
| 1 |
"""Plugwise Select component for Home Assistant."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
|
|
|
| 4 |
|
| 5 |
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
| 6 |
from homeassistant.const import STATE_ON, EntityCategory
|
|
|
|
| 8 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 9 |
|
| 10 |
from .const import (
|
| 11 |
+
AVAILABLE_SCHEDULES,
|
| 12 |
+
DHW_MODES,
|
| 13 |
+
GATEWAY_MODES,
|
| 14 |
+
LOCATION,
|
| 15 |
+
LOGGER,
|
| 16 |
+
REGULATION_MODES,
|
| 17 |
SELECT_DHW_MODE,
|
| 18 |
SELECT_GATEWAY_MODE,
|
| 19 |
SELECT_REGULATION_MODE,
|
| 20 |
SELECT_SCHEDULE,
|
| 21 |
SELECT_ZONE_PROFILE,
|
| 22 |
+
ZONE_PROFILES,
|
| 23 |
SelectOptionsType,
|
| 24 |
SelectType,
|
| 25 |
)
|
| 26 |
+
|
| 27 |
+
# Upstream consts
|
| 28 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 29 |
from .entity import PlugwiseEntity
|
| 30 |
from .util import plugwise_command
|
|
|
|
| 40 |
options_key: SelectOptionsType
|
| 41 |
|
| 42 |
|
| 43 |
+
# Upstream
|
| 44 |
SELECT_TYPES = (
|
| 45 |
PlugwiseSelectEntityDescription(
|
| 46 |
key=SELECT_SCHEDULE,
|
| 47 |
translation_key=SELECT_SCHEDULE,
|
| 48 |
+
options_key=AVAILABLE_SCHEDULES,
|
| 49 |
),
|
| 50 |
PlugwiseSelectEntityDescription(
|
| 51 |
key=SELECT_REGULATION_MODE,
|
| 52 |
translation_key=SELECT_REGULATION_MODE,
|
| 53 |
entity_category=EntityCategory.CONFIG,
|
| 54 |
+
options_key=REGULATION_MODES,
|
| 55 |
),
|
| 56 |
PlugwiseSelectEntityDescription(
|
| 57 |
key=SELECT_DHW_MODE,
|
| 58 |
translation_key=SELECT_DHW_MODE,
|
| 59 |
entity_category=EntityCategory.CONFIG,
|
| 60 |
+
options_key=DHW_MODES,
|
| 61 |
),
|
| 62 |
PlugwiseSelectEntityDescription(
|
| 63 |
key=SELECT_GATEWAY_MODE,
|
| 64 |
translation_key=SELECT_GATEWAY_MODE,
|
| 65 |
entity_category=EntityCategory.CONFIG,
|
| 66 |
+
options_key=GATEWAY_MODES,
|
| 67 |
),
|
| 68 |
PlugwiseSelectEntityDescription(
|
| 69 |
key=SELECT_ZONE_PROFILE,
|
| 70 |
translation_key=SELECT_ZONE_PROFILE,
|
| 71 |
entity_category=EntityCategory.CONFIG,
|
| 72 |
+
options_key=ZONE_PROFILES,
|
| 73 |
),
|
| 74 |
)
|
| 75 |
|
|
|
|
| 79 |
entry: PlugwiseConfigEntry,
|
| 80 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 81 |
) -> None:
|
| 82 |
+
"""Set up Plugwise selector from a config entry."""
|
| 83 |
coordinator = entry.runtime_data
|
| 84 |
|
| 85 |
@callback
|
|
|
|
| 88 |
if not coordinator.new_devices:
|
| 89 |
return
|
| 90 |
|
| 91 |
+
# Upstream consts
|
| 92 |
+
# async_add_entities(
|
| 93 |
+
# PlugwiseSelectEntity(coordinator, device_id, description)
|
| 94 |
+
# for device_id in coordinator.new_devices
|
| 95 |
+
# for description in SELECT_TYPES
|
| 96 |
+
# if description.options_key in coordinator.data.devices[device_id]
|
| 97 |
+
# )
|
| 98 |
+
# pw-beta alternative for debugging
|
| 99 |
+
entities: list[PlugwiseSelectEntity] = []
|
| 100 |
+
for device_id in coordinator.new_devices:
|
| 101 |
+
device = coordinator.data[device_id]
|
| 102 |
+
for description in SELECT_TYPES:
|
| 103 |
+
if device.get(description.options_key):
|
| 104 |
+
entities.append(
|
| 105 |
+
PlugwiseSelectEntity(coordinator, device_id, description)
|
| 106 |
+
)
|
| 107 |
+
LOGGER.debug(
|
| 108 |
+
"Add %s %s selector", device["name"], description.translation_key
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
async_add_entities(entities)
|
| 112 |
|
| 113 |
_add_entities()
|
| 114 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
|
|
| 130 |
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
| 131 |
self.entity_description = entity_description
|
| 132 |
|
| 133 |
+
self._device_or_location = device_id
|
| 134 |
+
if (
|
| 135 |
+
self.entity_description.key in (SELECT_SCHEDULE, SELECT_ZONE_PROFILE)
|
| 136 |
+
and (location := self.device.get(LOCATION)) is not None
|
| 137 |
+
):
|
| 138 |
+
self._device_or_location = location
|
| 139 |
|
| 140 |
@property
|
|
|
|
| 141 |
def current_option(self) -> str | None:
|
| 142 |
"""Return the selected entity option to represent the entity state."""
|
| 143 |
+
return self.device.get(self.entity_description.key)
|
| 144 |
|
| 145 |
@property
|
|
|
|
| 146 |
def options(self) -> list[str]:
|
| 147 |
"""Return the available select-options."""
|
| 148 |
+
return self.device.get(self.entity_description.options_key, [])
|
| 149 |
|
| 150 |
@plugwise_command
|
|
|
|
| 151 |
async def async_select_option(self, option: str) -> None:
|
| 152 |
"""Change to the selected entity option.
|
| 153 |
|
| 154 |
+
Appliance ID (= device_id) is required for the dhw_mode select
|
| 155 |
+
Locattion ID is required for the thermostat-schedule and zone_profile selects.
|
| 156 |
+
STATE_ON is required for the thermostat-schedule select.
|
| 157 |
"""
|
| 158 |
await self.coordinator.api.set_select(
|
| 159 |
+
self.entity_description.key, self._device_or_location, option, STATE_ON
|
| 160 |
+
)
|
| 161 |
+
LOGGER.debug(
|
| 162 |
+
"Set %s to %s was successful",
|
| 163 |
+
self.entity_description.key,
|
| 164 |
+
option,
|
| 165 |
)
|
|
@@ -1,7 +1,6 @@
|
|
| 1 |
"""Plugwise Sensor component for Home Assistant."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
-
from typing import override
|
| 5 |
|
| 6 |
from plugwise.constants import SensorType
|
| 7 |
|
|
@@ -12,6 +11,7 @@
|
|
| 12 |
SensorStateClass,
|
| 13 |
)
|
| 14 |
from homeassistant.const import (
|
|
|
|
| 15 |
LIGHT_LUX,
|
| 16 |
PERCENTAGE,
|
| 17 |
EntityCategory,
|
|
@@ -26,6 +26,57 @@
|
|
| 26 |
from homeassistant.core import HomeAssistant, callback
|
| 27 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 30 |
from .entity import PlugwiseEntity
|
| 31 |
|
|
@@ -40,299 +91,301 @@
|
|
| 40 |
key: SensorType
|
| 41 |
|
| 42 |
|
| 43 |
-
|
|
|
|
| 44 |
PlugwiseSensorEntityDescription(
|
| 45 |
-
key=
|
| 46 |
-
translation_key=
|
| 47 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 48 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 49 |
state_class=SensorStateClass.MEASUREMENT,
|
| 50 |
),
|
| 51 |
PlugwiseSensorEntityDescription(
|
| 52 |
-
key=
|
| 53 |
translation_key="cooling_setpoint",
|
| 54 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 55 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 56 |
state_class=SensorStateClass.MEASUREMENT,
|
| 57 |
),
|
| 58 |
PlugwiseSensorEntityDescription(
|
| 59 |
-
key=
|
| 60 |
translation_key="heating_setpoint",
|
| 61 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 62 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 63 |
state_class=SensorStateClass.MEASUREMENT,
|
| 64 |
),
|
| 65 |
PlugwiseSensorEntityDescription(
|
| 66 |
-
key=
|
| 67 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 68 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 69 |
state_class=SensorStateClass.MEASUREMENT,
|
| 70 |
),
|
| 71 |
PlugwiseSensorEntityDescription(
|
| 72 |
-
key=
|
| 73 |
-
translation_key=
|
| 74 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 75 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 76 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 77 |
state_class=SensorStateClass.MEASUREMENT,
|
| 78 |
),
|
| 79 |
PlugwiseSensorEntityDescription(
|
| 80 |
-
key=
|
| 81 |
-
translation_key=
|
| 82 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 83 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 84 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 85 |
state_class=SensorStateClass.MEASUREMENT,
|
| 86 |
),
|
| 87 |
PlugwiseSensorEntityDescription(
|
| 88 |
-
key=
|
| 89 |
-
translation_key=
|
| 90 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 91 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 92 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 93 |
state_class=SensorStateClass.MEASUREMENT,
|
|
|
|
| 94 |
),
|
| 95 |
PlugwiseSensorEntityDescription(
|
| 96 |
-
key=
|
| 97 |
-
translation_key=
|
| 98 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 99 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 100 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 101 |
state_class=SensorStateClass.MEASUREMENT,
|
| 102 |
),
|
| 103 |
PlugwiseSensorEntityDescription(
|
| 104 |
-
key=
|
| 105 |
-
translation_key=
|
| 106 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 107 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 108 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 109 |
state_class=SensorStateClass.MEASUREMENT,
|
| 110 |
),
|
| 111 |
PlugwiseSensorEntityDescription(
|
| 112 |
-
key=
|
| 113 |
-
translation_key=
|
| 114 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 115 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 116 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 117 |
state_class=SensorStateClass.MEASUREMENT,
|
| 118 |
),
|
| 119 |
PlugwiseSensorEntityDescription(
|
| 120 |
-
key=
|
| 121 |
-
translation_key=
|
| 122 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 123 |
device_class=SensorDeviceClass.POWER,
|
| 124 |
state_class=SensorStateClass.MEASUREMENT,
|
| 125 |
),
|
| 126 |
PlugwiseSensorEntityDescription(
|
| 127 |
-
key=
|
| 128 |
-
translation_key=
|
| 129 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 130 |
device_class=SensorDeviceClass.POWER,
|
| 131 |
state_class=SensorStateClass.MEASUREMENT,
|
| 132 |
entity_registry_enabled_default=False,
|
| 133 |
),
|
| 134 |
PlugwiseSensorEntityDescription(
|
| 135 |
-
key=
|
| 136 |
-
translation_key=
|
| 137 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 138 |
device_class=SensorDeviceClass.ENERGY,
|
| 139 |
state_class=SensorStateClass.TOTAL,
|
| 140 |
),
|
| 141 |
PlugwiseSensorEntityDescription(
|
| 142 |
-
key=
|
| 143 |
-
translation_key=
|
| 144 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 145 |
device_class=SensorDeviceClass.ENERGY,
|
| 146 |
state_class=SensorStateClass.TOTAL,
|
| 147 |
),
|
| 148 |
PlugwiseSensorEntityDescription(
|
| 149 |
-
key=
|
| 150 |
-
translation_key=
|
| 151 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 152 |
device_class=SensorDeviceClass.ENERGY,
|
| 153 |
state_class=SensorStateClass.TOTAL,
|
| 154 |
),
|
| 155 |
PlugwiseSensorEntityDescription(
|
| 156 |
-
key=
|
| 157 |
-
translation_key=
|
| 158 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 159 |
device_class=SensorDeviceClass.ENERGY,
|
| 160 |
state_class=SensorStateClass.TOTAL,
|
| 161 |
entity_registry_enabled_default=False,
|
| 162 |
),
|
| 163 |
PlugwiseSensorEntityDescription(
|
| 164 |
-
key=
|
| 165 |
-
translation_key=
|
| 166 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 167 |
device_class=SensorDeviceClass.ENERGY,
|
| 168 |
state_class=SensorStateClass.TOTAL,
|
| 169 |
),
|
| 170 |
PlugwiseSensorEntityDescription(
|
| 171 |
-
key=
|
| 172 |
-
translation_key=
|
| 173 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 174 |
device_class=SensorDeviceClass.ENERGY,
|
| 175 |
state_class=SensorStateClass.TOTAL,
|
| 176 |
),
|
| 177 |
PlugwiseSensorEntityDescription(
|
| 178 |
-
key=
|
| 179 |
-
translation_key=
|
| 180 |
device_class=SensorDeviceClass.POWER,
|
| 181 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 182 |
state_class=SensorStateClass.MEASUREMENT,
|
| 183 |
),
|
| 184 |
PlugwiseSensorEntityDescription(
|
| 185 |
-
key=
|
| 186 |
-
translation_key=
|
| 187 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 188 |
device_class=SensorDeviceClass.POWER,
|
| 189 |
state_class=SensorStateClass.MEASUREMENT,
|
| 190 |
),
|
| 191 |
PlugwiseSensorEntityDescription(
|
| 192 |
-
key=
|
| 193 |
-
translation_key=
|
| 194 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 195 |
device_class=SensorDeviceClass.POWER,
|
| 196 |
state_class=SensorStateClass.MEASUREMENT,
|
| 197 |
),
|
| 198 |
PlugwiseSensorEntityDescription(
|
| 199 |
-
key=
|
| 200 |
-
translation_key=
|
| 201 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 202 |
device_class=SensorDeviceClass.ENERGY,
|
| 203 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 204 |
),
|
| 205 |
PlugwiseSensorEntityDescription(
|
| 206 |
-
key=
|
| 207 |
-
translation_key=
|
| 208 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 209 |
device_class=SensorDeviceClass.ENERGY,
|
| 210 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 211 |
),
|
| 212 |
PlugwiseSensorEntityDescription(
|
| 213 |
-
key=
|
| 214 |
-
translation_key=
|
| 215 |
device_class=SensorDeviceClass.POWER,
|
| 216 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 217 |
state_class=SensorStateClass.MEASUREMENT,
|
| 218 |
),
|
| 219 |
PlugwiseSensorEntityDescription(
|
| 220 |
-
key=
|
| 221 |
-
translation_key=
|
| 222 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 223 |
device_class=SensorDeviceClass.POWER,
|
| 224 |
state_class=SensorStateClass.MEASUREMENT,
|
| 225 |
),
|
| 226 |
PlugwiseSensorEntityDescription(
|
| 227 |
-
key=
|
| 228 |
-
translation_key=
|
| 229 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 230 |
device_class=SensorDeviceClass.POWER,
|
| 231 |
state_class=SensorStateClass.MEASUREMENT,
|
| 232 |
),
|
| 233 |
PlugwiseSensorEntityDescription(
|
| 234 |
-
key=
|
| 235 |
-
translation_key=
|
| 236 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 237 |
device_class=SensorDeviceClass.ENERGY,
|
| 238 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 239 |
),
|
| 240 |
PlugwiseSensorEntityDescription(
|
| 241 |
-
key=
|
| 242 |
-
translation_key=
|
| 243 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 244 |
device_class=SensorDeviceClass.ENERGY,
|
| 245 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 246 |
),
|
| 247 |
PlugwiseSensorEntityDescription(
|
| 248 |
-
key=
|
| 249 |
-
translation_key=
|
| 250 |
device_class=SensorDeviceClass.POWER,
|
| 251 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 252 |
state_class=SensorStateClass.MEASUREMENT,
|
| 253 |
),
|
| 254 |
PlugwiseSensorEntityDescription(
|
| 255 |
-
key=
|
| 256 |
-
translation_key=
|
| 257 |
device_class=SensorDeviceClass.POWER,
|
| 258 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 259 |
state_class=SensorStateClass.MEASUREMENT,
|
| 260 |
),
|
| 261 |
PlugwiseSensorEntityDescription(
|
| 262 |
-
key=
|
| 263 |
-
translation_key=
|
| 264 |
device_class=SensorDeviceClass.POWER,
|
| 265 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 266 |
state_class=SensorStateClass.MEASUREMENT,
|
| 267 |
),
|
| 268 |
PlugwiseSensorEntityDescription(
|
| 269 |
-
key=
|
| 270 |
-
translation_key=
|
| 271 |
device_class=SensorDeviceClass.POWER,
|
| 272 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 273 |
state_class=SensorStateClass.MEASUREMENT,
|
| 274 |
),
|
| 275 |
PlugwiseSensorEntityDescription(
|
| 276 |
-
key=
|
| 277 |
-
translation_key=
|
| 278 |
device_class=SensorDeviceClass.POWER,
|
| 279 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 280 |
state_class=SensorStateClass.MEASUREMENT,
|
| 281 |
),
|
| 282 |
PlugwiseSensorEntityDescription(
|
| 283 |
-
key=
|
| 284 |
-
translation_key=
|
| 285 |
device_class=SensorDeviceClass.POWER,
|
| 286 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 287 |
state_class=SensorStateClass.MEASUREMENT,
|
| 288 |
),
|
| 289 |
PlugwiseSensorEntityDescription(
|
| 290 |
-
key=
|
| 291 |
-
translation_key=
|
| 292 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 293 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
| 294 |
state_class=SensorStateClass.MEASUREMENT,
|
| 295 |
entity_registry_enabled_default=False,
|
| 296 |
),
|
| 297 |
PlugwiseSensorEntityDescription(
|
| 298 |
-
key=
|
| 299 |
-
translation_key=
|
| 300 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 301 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
| 302 |
state_class=SensorStateClass.MEASUREMENT,
|
| 303 |
entity_registry_enabled_default=False,
|
| 304 |
),
|
| 305 |
PlugwiseSensorEntityDescription(
|
| 306 |
-
key=
|
| 307 |
-
translation_key=
|
| 308 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 309 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
| 310 |
state_class=SensorStateClass.MEASUREMENT,
|
| 311 |
entity_registry_enabled_default=False,
|
| 312 |
),
|
| 313 |
PlugwiseSensorEntityDescription(
|
| 314 |
-
key=
|
| 315 |
-
translation_key=
|
| 316 |
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
| 317 |
state_class=SensorStateClass.MEASUREMENT,
|
| 318 |
),
|
| 319 |
PlugwiseSensorEntityDescription(
|
| 320 |
-
key=
|
| 321 |
-
translation_key=
|
| 322 |
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
| 323 |
device_class=SensorDeviceClass.GAS,
|
| 324 |
state_class=SensorStateClass.TOTAL,
|
| 325 |
),
|
| 326 |
PlugwiseSensorEntityDescription(
|
| 327 |
-
key=
|
| 328 |
-
translation_key=
|
| 329 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 330 |
device_class=SensorDeviceClass.POWER,
|
| 331 |
state_class=SensorStateClass.MEASUREMENT,
|
| 332 |
),
|
| 333 |
PlugwiseSensorEntityDescription(
|
| 334 |
-
key=
|
| 335 |
-
translation_key=
|
| 336 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 337 |
device_class=SensorDeviceClass.ENERGY,
|
| 338 |
state_class=SensorStateClass.TOTAL,
|
|
@@ -352,22 +405,22 @@
|
|
| 352 |
state_class=SensorStateClass.MEASUREMENT,
|
| 353 |
),
|
| 354 |
PlugwiseSensorEntityDescription(
|
| 355 |
-
key=
|
| 356 |
-
translation_key=
|
| 357 |
native_unit_of_measurement=PERCENTAGE,
|
| 358 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 359 |
state_class=SensorStateClass.MEASUREMENT,
|
| 360 |
),
|
| 361 |
PlugwiseSensorEntityDescription(
|
| 362 |
-
key=
|
| 363 |
-
translation_key=
|
| 364 |
native_unit_of_measurement=PERCENTAGE,
|
| 365 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 366 |
state_class=SensorStateClass.MEASUREMENT,
|
| 367 |
),
|
| 368 |
PlugwiseSensorEntityDescription(
|
| 369 |
-
key=
|
| 370 |
-
translation_key=
|
| 371 |
native_unit_of_measurement=UnitOfPressure.BAR,
|
| 372 |
device_class=SensorDeviceClass.PRESSURE,
|
| 373 |
entity_category=EntityCategory.DIAGNOSTIC,
|
|
@@ -380,16 +433,16 @@
|
|
| 380 |
state_class=SensorStateClass.MEASUREMENT,
|
| 381 |
),
|
| 382 |
PlugwiseSensorEntityDescription(
|
| 383 |
-
key=
|
| 384 |
-
translation_key=
|
| 385 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 386 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 387 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 388 |
state_class=SensorStateClass.MEASUREMENT,
|
| 389 |
),
|
| 390 |
PlugwiseSensorEntityDescription(
|
| 391 |
-
key=
|
| 392 |
-
translation_key=
|
| 393 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 394 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 395 |
entity_category=EntityCategory.DIAGNOSTIC,
|
|
@@ -403,7 +456,8 @@
|
|
| 403 |
entry: PlugwiseConfigEntry,
|
| 404 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 405 |
) -> None:
|
| 406 |
-
"""Set up
|
|
|
|
| 407 |
coordinator = entry.runtime_data
|
| 408 |
|
| 409 |
@callback
|
|
@@ -412,13 +466,29 @@
|
|
| 412 |
if not coordinator.new_devices:
|
| 413 |
return
|
| 414 |
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
_add_entities()
|
| 424 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
@@ -437,11 +507,10 @@
|
|
| 437 |
) -> None:
|
| 438 |
"""Initialise the sensor."""
|
| 439 |
super().__init__(coordinator, device_id)
|
| 440 |
-
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 441 |
self.entity_description = description
|
|
|
|
| 442 |
|
| 443 |
@property
|
| 444 |
-
|
| 445 |
-
def native_value(self) -> int | float:
|
| 446 |
"""Return the value reported by the sensor."""
|
| 447 |
-
return self.device
|
|
|
|
| 1 |
"""Plugwise Sensor component for Home Assistant."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
|
|
|
| 4 |
|
| 5 |
from plugwise.constants import SensorType
|
| 6 |
|
|
|
|
| 11 |
SensorStateClass,
|
| 12 |
)
|
| 13 |
from homeassistant.const import (
|
| 14 |
+
ATTR_TEMPERATURE, # Upstream
|
| 15 |
LIGHT_LUX,
|
| 16 |
PERCENTAGE,
|
| 17 |
EntityCategory,
|
|
|
|
| 26 |
from homeassistant.core import HomeAssistant, callback
|
| 27 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 28 |
|
| 29 |
+
from .const import (
|
| 30 |
+
DHW_SETPOINT,
|
| 31 |
+
DHW_TEMP,
|
| 32 |
+
EL_CONS_INTERVAL,
|
| 33 |
+
EL_CONS_OP_CUMULATIVE,
|
| 34 |
+
EL_CONS_OP_INTERVAL,
|
| 35 |
+
EL_CONS_OP_POINT,
|
| 36 |
+
EL_CONS_P_CUMULATIVE,
|
| 37 |
+
EL_CONS_P_INTERVAL,
|
| 38 |
+
EL_CONS_P_POINT,
|
| 39 |
+
EL_CONS_POINT,
|
| 40 |
+
EL_CONSUMED,
|
| 41 |
+
EL_PH1_CONSUMED,
|
| 42 |
+
EL_PH1_PRODUCED,
|
| 43 |
+
EL_PH2_CONSUMED,
|
| 44 |
+
EL_PH2_PRODUCED,
|
| 45 |
+
EL_PH3_CONSUMED,
|
| 46 |
+
EL_PH3_PRODUCED,
|
| 47 |
+
EL_PROD_INTERVAL,
|
| 48 |
+
EL_PROD_OP_CUMULATIVE,
|
| 49 |
+
EL_PROD_OP_INTERVAL,
|
| 50 |
+
EL_PROD_OP_POINT,
|
| 51 |
+
EL_PROD_P_CUMULATIVE,
|
| 52 |
+
EL_PROD_P_INTERVAL,
|
| 53 |
+
EL_PROD_P_POINT,
|
| 54 |
+
EL_PROD_POINT,
|
| 55 |
+
EL_PRODUCED,
|
| 56 |
+
GAS_CONS_CUMULATIVE,
|
| 57 |
+
GAS_CONS_INTERVAL,
|
| 58 |
+
INTENDED_BOILER_TEMP,
|
| 59 |
+
LOGGER, # pw-beta
|
| 60 |
+
MOD_LEVEL,
|
| 61 |
+
NET_EL_CUMULATIVE,
|
| 62 |
+
NET_EL_POINT,
|
| 63 |
+
OUTDOOR_AIR_TEMP,
|
| 64 |
+
OUTDOOR_TEMP,
|
| 65 |
+
RETURN_TEMP,
|
| 66 |
+
SENSORS,
|
| 67 |
+
TARGET_TEMP,
|
| 68 |
+
TARGET_TEMP_HIGH,
|
| 69 |
+
TARGET_TEMP_LOW,
|
| 70 |
+
TEMP_DIFF,
|
| 71 |
+
VALVE_POS,
|
| 72 |
+
VOLTAGE_PH1,
|
| 73 |
+
VOLTAGE_PH2,
|
| 74 |
+
VOLTAGE_PH3,
|
| 75 |
+
WATER_PRESSURE,
|
| 76 |
+
WATER_TEMP,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# Upstream consts
|
| 80 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 81 |
from .entity import PlugwiseEntity
|
| 82 |
|
|
|
|
| 91 |
key: SensorType
|
| 92 |
|
| 93 |
|
| 94 |
+
# Upstream consts
|
| 95 |
+
PLUGWISE_SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
|
| 96 |
PlugwiseSensorEntityDescription(
|
| 97 |
+
key=TARGET_TEMP,
|
| 98 |
+
translation_key=TARGET_TEMP,
|
| 99 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 100 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 101 |
state_class=SensorStateClass.MEASUREMENT,
|
| 102 |
),
|
| 103 |
PlugwiseSensorEntityDescription(
|
| 104 |
+
key=TARGET_TEMP_HIGH,
|
| 105 |
translation_key="cooling_setpoint",
|
| 106 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 107 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 108 |
state_class=SensorStateClass.MEASUREMENT,
|
| 109 |
),
|
| 110 |
PlugwiseSensorEntityDescription(
|
| 111 |
+
key=TARGET_TEMP_LOW,
|
| 112 |
translation_key="heating_setpoint",
|
| 113 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 114 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 115 |
state_class=SensorStateClass.MEASUREMENT,
|
| 116 |
),
|
| 117 |
PlugwiseSensorEntityDescription(
|
| 118 |
+
key=ATTR_TEMPERATURE,
|
| 119 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 120 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 121 |
state_class=SensorStateClass.MEASUREMENT,
|
| 122 |
),
|
| 123 |
PlugwiseSensorEntityDescription(
|
| 124 |
+
key=INTENDED_BOILER_TEMP,
|
| 125 |
+
translation_key=INTENDED_BOILER_TEMP,
|
| 126 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 127 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 128 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 129 |
state_class=SensorStateClass.MEASUREMENT,
|
| 130 |
),
|
| 131 |
PlugwiseSensorEntityDescription(
|
| 132 |
+
key=TEMP_DIFF,
|
| 133 |
+
translation_key=TEMP_DIFF,
|
| 134 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 135 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 136 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 137 |
state_class=SensorStateClass.MEASUREMENT,
|
| 138 |
),
|
| 139 |
PlugwiseSensorEntityDescription(
|
| 140 |
+
key=OUTDOOR_TEMP,
|
| 141 |
+
translation_key=OUTDOOR_TEMP,
|
| 142 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 143 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 144 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 145 |
state_class=SensorStateClass.MEASUREMENT,
|
| 146 |
+
suggested_display_precision=1,
|
| 147 |
),
|
| 148 |
PlugwiseSensorEntityDescription(
|
| 149 |
+
key=OUTDOOR_AIR_TEMP,
|
| 150 |
+
translation_key=OUTDOOR_AIR_TEMP,
|
| 151 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 152 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 153 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 154 |
state_class=SensorStateClass.MEASUREMENT,
|
| 155 |
),
|
| 156 |
PlugwiseSensorEntityDescription(
|
| 157 |
+
key=WATER_TEMP,
|
| 158 |
+
translation_key=WATER_TEMP,
|
| 159 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 160 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 161 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 162 |
state_class=SensorStateClass.MEASUREMENT,
|
| 163 |
),
|
| 164 |
PlugwiseSensorEntityDescription(
|
| 165 |
+
key=RETURN_TEMP,
|
| 166 |
+
translation_key=RETURN_TEMP,
|
| 167 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 168 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 169 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 170 |
state_class=SensorStateClass.MEASUREMENT,
|
| 171 |
),
|
| 172 |
PlugwiseSensorEntityDescription(
|
| 173 |
+
key=EL_CONSUMED,
|
| 174 |
+
translation_key=EL_CONSUMED,
|
| 175 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 176 |
device_class=SensorDeviceClass.POWER,
|
| 177 |
state_class=SensorStateClass.MEASUREMENT,
|
| 178 |
),
|
| 179 |
PlugwiseSensorEntityDescription(
|
| 180 |
+
key=EL_PRODUCED,
|
| 181 |
+
translation_key=EL_PRODUCED,
|
| 182 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 183 |
device_class=SensorDeviceClass.POWER,
|
| 184 |
state_class=SensorStateClass.MEASUREMENT,
|
| 185 |
entity_registry_enabled_default=False,
|
| 186 |
),
|
| 187 |
PlugwiseSensorEntityDescription(
|
| 188 |
+
key=EL_CONS_INTERVAL,
|
| 189 |
+
translation_key=EL_CONS_INTERVAL,
|
| 190 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 191 |
device_class=SensorDeviceClass.ENERGY,
|
| 192 |
state_class=SensorStateClass.TOTAL,
|
| 193 |
),
|
| 194 |
PlugwiseSensorEntityDescription(
|
| 195 |
+
key=EL_CONS_P_INTERVAL,
|
| 196 |
+
translation_key=EL_CONS_P_INTERVAL,
|
| 197 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 198 |
device_class=SensorDeviceClass.ENERGY,
|
| 199 |
state_class=SensorStateClass.TOTAL,
|
| 200 |
),
|
| 201 |
PlugwiseSensorEntityDescription(
|
| 202 |
+
key=EL_CONS_OP_INTERVAL,
|
| 203 |
+
translation_key=EL_CONS_OP_INTERVAL,
|
| 204 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 205 |
device_class=SensorDeviceClass.ENERGY,
|
| 206 |
state_class=SensorStateClass.TOTAL,
|
| 207 |
),
|
| 208 |
PlugwiseSensorEntityDescription(
|
| 209 |
+
key=EL_PROD_INTERVAL,
|
| 210 |
+
translation_key=EL_PROD_INTERVAL,
|
| 211 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 212 |
device_class=SensorDeviceClass.ENERGY,
|
| 213 |
state_class=SensorStateClass.TOTAL,
|
| 214 |
entity_registry_enabled_default=False,
|
| 215 |
),
|
| 216 |
PlugwiseSensorEntityDescription(
|
| 217 |
+
key=EL_PROD_P_INTERVAL,
|
| 218 |
+
translation_key=EL_PROD_P_INTERVAL,
|
| 219 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 220 |
device_class=SensorDeviceClass.ENERGY,
|
| 221 |
state_class=SensorStateClass.TOTAL,
|
| 222 |
),
|
| 223 |
PlugwiseSensorEntityDescription(
|
| 224 |
+
key=EL_PROD_OP_INTERVAL,
|
| 225 |
+
translation_key=EL_PROD_OP_INTERVAL,
|
| 226 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 227 |
device_class=SensorDeviceClass.ENERGY,
|
| 228 |
state_class=SensorStateClass.TOTAL,
|
| 229 |
),
|
| 230 |
PlugwiseSensorEntityDescription(
|
| 231 |
+
key=EL_CONS_POINT,
|
| 232 |
+
translation_key=EL_CONS_POINT,
|
| 233 |
device_class=SensorDeviceClass.POWER,
|
| 234 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 235 |
state_class=SensorStateClass.MEASUREMENT,
|
| 236 |
),
|
| 237 |
PlugwiseSensorEntityDescription(
|
| 238 |
+
key=EL_CONS_OP_POINT,
|
| 239 |
+
translation_key=EL_CONS_OP_POINT,
|
| 240 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 241 |
device_class=SensorDeviceClass.POWER,
|
| 242 |
state_class=SensorStateClass.MEASUREMENT,
|
| 243 |
),
|
| 244 |
PlugwiseSensorEntityDescription(
|
| 245 |
+
key=EL_CONS_P_POINT,
|
| 246 |
+
translation_key=EL_CONS_P_POINT,
|
| 247 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 248 |
device_class=SensorDeviceClass.POWER,
|
| 249 |
state_class=SensorStateClass.MEASUREMENT,
|
| 250 |
),
|
| 251 |
PlugwiseSensorEntityDescription(
|
| 252 |
+
key=EL_CONS_OP_CUMULATIVE,
|
| 253 |
+
translation_key=EL_CONS_OP_CUMULATIVE,
|
| 254 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 255 |
device_class=SensorDeviceClass.ENERGY,
|
| 256 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 257 |
),
|
| 258 |
PlugwiseSensorEntityDescription(
|
| 259 |
+
key=EL_CONS_P_CUMULATIVE,
|
| 260 |
+
translation_key=EL_CONS_P_CUMULATIVE,
|
| 261 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 262 |
device_class=SensorDeviceClass.ENERGY,
|
| 263 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 264 |
),
|
| 265 |
PlugwiseSensorEntityDescription(
|
| 266 |
+
key=EL_PROD_POINT,
|
| 267 |
+
translation_key=EL_PROD_POINT,
|
| 268 |
device_class=SensorDeviceClass.POWER,
|
| 269 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 270 |
state_class=SensorStateClass.MEASUREMENT,
|
| 271 |
),
|
| 272 |
PlugwiseSensorEntityDescription(
|
| 273 |
+
key=EL_PROD_OP_POINT,
|
| 274 |
+
translation_key=EL_PROD_OP_POINT,
|
| 275 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 276 |
device_class=SensorDeviceClass.POWER,
|
| 277 |
state_class=SensorStateClass.MEASUREMENT,
|
| 278 |
),
|
| 279 |
PlugwiseSensorEntityDescription(
|
| 280 |
+
key=EL_PROD_P_POINT,
|
| 281 |
+
translation_key=EL_PROD_P_POINT,
|
| 282 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 283 |
device_class=SensorDeviceClass.POWER,
|
| 284 |
state_class=SensorStateClass.MEASUREMENT,
|
| 285 |
),
|
| 286 |
PlugwiseSensorEntityDescription(
|
| 287 |
+
key=EL_PROD_OP_CUMULATIVE,
|
| 288 |
+
translation_key=EL_PROD_OP_CUMULATIVE,
|
| 289 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 290 |
device_class=SensorDeviceClass.ENERGY,
|
| 291 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 292 |
),
|
| 293 |
PlugwiseSensorEntityDescription(
|
| 294 |
+
key=EL_PROD_P_CUMULATIVE,
|
| 295 |
+
translation_key=EL_PROD_P_CUMULATIVE,
|
| 296 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 297 |
device_class=SensorDeviceClass.ENERGY,
|
| 298 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 299 |
),
|
| 300 |
PlugwiseSensorEntityDescription(
|
| 301 |
+
key=EL_PH1_CONSUMED,
|
| 302 |
+
translation_key=EL_PH1_CONSUMED,
|
| 303 |
device_class=SensorDeviceClass.POWER,
|
| 304 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 305 |
state_class=SensorStateClass.MEASUREMENT,
|
| 306 |
),
|
| 307 |
PlugwiseSensorEntityDescription(
|
| 308 |
+
key=EL_PH2_CONSUMED,
|
| 309 |
+
translation_key=EL_PH2_CONSUMED,
|
| 310 |
device_class=SensorDeviceClass.POWER,
|
| 311 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 312 |
state_class=SensorStateClass.MEASUREMENT,
|
| 313 |
),
|
| 314 |
PlugwiseSensorEntityDescription(
|
| 315 |
+
key=EL_PH3_CONSUMED,
|
| 316 |
+
translation_key=EL_PH3_CONSUMED,
|
| 317 |
device_class=SensorDeviceClass.POWER,
|
| 318 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 319 |
state_class=SensorStateClass.MEASUREMENT,
|
| 320 |
),
|
| 321 |
PlugwiseSensorEntityDescription(
|
| 322 |
+
key=EL_PH1_PRODUCED,
|
| 323 |
+
translation_key=EL_PH1_PRODUCED,
|
| 324 |
device_class=SensorDeviceClass.POWER,
|
| 325 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 326 |
state_class=SensorStateClass.MEASUREMENT,
|
| 327 |
),
|
| 328 |
PlugwiseSensorEntityDescription(
|
| 329 |
+
key=EL_PH2_PRODUCED,
|
| 330 |
+
translation_key=EL_PH2_PRODUCED,
|
| 331 |
device_class=SensorDeviceClass.POWER,
|
| 332 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 333 |
state_class=SensorStateClass.MEASUREMENT,
|
| 334 |
),
|
| 335 |
PlugwiseSensorEntityDescription(
|
| 336 |
+
key=EL_PH3_PRODUCED,
|
| 337 |
+
translation_key=EL_PH3_PRODUCED,
|
| 338 |
device_class=SensorDeviceClass.POWER,
|
| 339 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 340 |
state_class=SensorStateClass.MEASUREMENT,
|
| 341 |
),
|
| 342 |
PlugwiseSensorEntityDescription(
|
| 343 |
+
key=VOLTAGE_PH1,
|
| 344 |
+
translation_key=VOLTAGE_PH1,
|
| 345 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 346 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
| 347 |
state_class=SensorStateClass.MEASUREMENT,
|
| 348 |
entity_registry_enabled_default=False,
|
| 349 |
),
|
| 350 |
PlugwiseSensorEntityDescription(
|
| 351 |
+
key=VOLTAGE_PH2,
|
| 352 |
+
translation_key=VOLTAGE_PH2,
|
| 353 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 354 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
| 355 |
state_class=SensorStateClass.MEASUREMENT,
|
| 356 |
entity_registry_enabled_default=False,
|
| 357 |
),
|
| 358 |
PlugwiseSensorEntityDescription(
|
| 359 |
+
key=VOLTAGE_PH3,
|
| 360 |
+
translation_key=VOLTAGE_PH3,
|
| 361 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 362 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
| 363 |
state_class=SensorStateClass.MEASUREMENT,
|
| 364 |
entity_registry_enabled_default=False,
|
| 365 |
),
|
| 366 |
PlugwiseSensorEntityDescription(
|
| 367 |
+
key=GAS_CONS_INTERVAL,
|
| 368 |
+
translation_key=GAS_CONS_INTERVAL,
|
| 369 |
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
| 370 |
state_class=SensorStateClass.MEASUREMENT,
|
| 371 |
),
|
| 372 |
PlugwiseSensorEntityDescription(
|
| 373 |
+
key=GAS_CONS_CUMULATIVE,
|
| 374 |
+
translation_key=GAS_CONS_CUMULATIVE,
|
| 375 |
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
| 376 |
device_class=SensorDeviceClass.GAS,
|
| 377 |
state_class=SensorStateClass.TOTAL,
|
| 378 |
),
|
| 379 |
PlugwiseSensorEntityDescription(
|
| 380 |
+
key=NET_EL_POINT,
|
| 381 |
+
translation_key=NET_EL_POINT,
|
| 382 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 383 |
device_class=SensorDeviceClass.POWER,
|
| 384 |
state_class=SensorStateClass.MEASUREMENT,
|
| 385 |
),
|
| 386 |
PlugwiseSensorEntityDescription(
|
| 387 |
+
key=NET_EL_CUMULATIVE,
|
| 388 |
+
translation_key=NET_EL_CUMULATIVE,
|
| 389 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 390 |
device_class=SensorDeviceClass.ENERGY,
|
| 391 |
state_class=SensorStateClass.TOTAL,
|
|
|
|
| 405 |
state_class=SensorStateClass.MEASUREMENT,
|
| 406 |
),
|
| 407 |
PlugwiseSensorEntityDescription(
|
| 408 |
+
key=MOD_LEVEL,
|
| 409 |
+
translation_key=MOD_LEVEL,
|
| 410 |
native_unit_of_measurement=PERCENTAGE,
|
| 411 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 412 |
state_class=SensorStateClass.MEASUREMENT,
|
| 413 |
),
|
| 414 |
PlugwiseSensorEntityDescription(
|
| 415 |
+
key=VALVE_POS,
|
| 416 |
+
translation_key=VALVE_POS,
|
| 417 |
native_unit_of_measurement=PERCENTAGE,
|
| 418 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 419 |
state_class=SensorStateClass.MEASUREMENT,
|
| 420 |
),
|
| 421 |
PlugwiseSensorEntityDescription(
|
| 422 |
+
key=WATER_PRESSURE,
|
| 423 |
+
translation_key=WATER_PRESSURE,
|
| 424 |
native_unit_of_measurement=UnitOfPressure.BAR,
|
| 425 |
device_class=SensorDeviceClass.PRESSURE,
|
| 426 |
entity_category=EntityCategory.DIAGNOSTIC,
|
|
|
|
| 433 |
state_class=SensorStateClass.MEASUREMENT,
|
| 434 |
),
|
| 435 |
PlugwiseSensorEntityDescription(
|
| 436 |
+
key=DHW_TEMP,
|
| 437 |
+
translation_key=DHW_TEMP,
|
| 438 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 439 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 440 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 441 |
state_class=SensorStateClass.MEASUREMENT,
|
| 442 |
),
|
| 443 |
PlugwiseSensorEntityDescription(
|
| 444 |
+
key=DHW_SETPOINT,
|
| 445 |
+
translation_key=DHW_SETPOINT,
|
| 446 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 447 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 448 |
entity_category=EntityCategory.DIAGNOSTIC,
|
|
|
|
| 456 |
entry: PlugwiseConfigEntry,
|
| 457 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 458 |
) -> None:
|
| 459 |
+
"""Set up Plugwise sensors from a config entry."""
|
| 460 |
+
# Upstream as Plugwise not Smile
|
| 461 |
coordinator = entry.runtime_data
|
| 462 |
|
| 463 |
@callback
|
|
|
|
| 466 |
if not coordinator.new_devices:
|
| 467 |
return
|
| 468 |
|
| 469 |
+
# Upstream consts
|
| 470 |
+
# async_add_entities(
|
| 471 |
+
# PlugwiseSensorEntity(coordinator, device_id, description)
|
| 472 |
+
# for device_id in coordinator.new_devices
|
| 473 |
+
# if (sensors := coordinator.data.devices[device_id].get(SENSORS))
|
| 474 |
+
# for description in PLUGWISE_SENSORS
|
| 475 |
+
# if description.key in sensors
|
| 476 |
+
# )
|
| 477 |
+
# pw-beta alternative for debugging
|
| 478 |
+
entities: list[PlugwiseSensorEntity] = []
|
| 479 |
+
for device_id in coordinator.new_devices:
|
| 480 |
+
device = coordinator.data[device_id]
|
| 481 |
+
if not (sensors := device.get(SENSORS)):
|
| 482 |
+
continue
|
| 483 |
+
for description in PLUGWISE_SENSORS:
|
| 484 |
+
if description.key not in sensors:
|
| 485 |
+
continue
|
| 486 |
+
entities.append(PlugwiseSensorEntity(coordinator, device_id, description))
|
| 487 |
+
LOGGER.debug(
|
| 488 |
+
"Add %s %s sensor", device["name"], description.translation_key or description.key
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
async_add_entities(entities)
|
| 492 |
|
| 493 |
_add_entities()
|
| 494 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
|
|
| 507 |
) -> None:
|
| 508 |
"""Initialise the sensor."""
|
| 509 |
super().__init__(coordinator, device_id)
|
|
|
|
| 510 |
self.entity_description = description
|
| 511 |
+
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 512 |
|
| 513 |
@property
|
| 514 |
+
def native_value(self) -> int | float | None:
|
|
|
|
| 515 |
"""Return the value reported by the sensor."""
|
| 516 |
+
return self.device.get(SENSORS, {}).get(self.entity_description.key) # Upstream consts
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""Plugwise Switch component for HomeAssistant."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
-
from typing import Any
|
| 5 |
|
| 6 |
from plugwise.constants import SwitchType
|
| 7 |
|
|
@@ -14,6 +14,16 @@
|
|
| 14 |
from homeassistant.core import HomeAssistant, callback
|
| 15 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 18 |
from .entity import PlugwiseEntity
|
| 19 |
from .util import plugwise_command
|
|
@@ -28,25 +38,23 @@
|
|
| 28 |
key: SwitchType
|
| 29 |
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
key="dhw_cm_switch",
|
| 34 |
-
translation_key="dhw_cm_switch",
|
| 35 |
-
entity_category=EntityCategory.CONFIG,
|
| 36 |
-
),
|
| 37 |
PlugwiseSwitchEntityDescription(
|
| 38 |
-
key=
|
| 39 |
-
translation_key=
|
|
|
|
| 40 |
entity_category=EntityCategory.CONFIG,
|
| 41 |
),
|
| 42 |
PlugwiseSwitchEntityDescription(
|
| 43 |
-
key=
|
| 44 |
-
translation_key=
|
| 45 |
device_class=SwitchDeviceClass.SWITCH,
|
| 46 |
),
|
| 47 |
PlugwiseSwitchEntityDescription(
|
| 48 |
-
key=
|
| 49 |
-
translation_key=
|
|
|
|
| 50 |
entity_category=EntityCategory.CONFIG,
|
| 51 |
),
|
| 52 |
)
|
|
@@ -57,7 +65,7 @@
|
|
| 57 |
entry: PlugwiseConfigEntry,
|
| 58 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 59 |
) -> None:
|
| 60 |
-
"""Set up
|
| 61 |
coordinator = entry.runtime_data
|
| 62 |
|
| 63 |
@callback
|
|
@@ -66,13 +74,28 @@
|
|
| 66 |
if not coordinator.new_devices:
|
| 67 |
return
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
_add_entities()
|
| 78 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
@@ -91,33 +114,30 @@
|
|
| 91 |
) -> None:
|
| 92 |
"""Set up the Plugwise API."""
|
| 93 |
super().__init__(coordinator, device_id)
|
| 94 |
-
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 95 |
self.entity_description = description
|
|
|
|
| 96 |
|
| 97 |
@property
|
| 98 |
-
|
| 99 |
-
def is_on(self) -> bool:
|
| 100 |
"""Return True if entity is on."""
|
| 101 |
-
return self.device
|
| 102 |
|
| 103 |
@plugwise_command
|
| 104 |
-
@override
|
| 105 |
async def async_turn_on(self, **kwargs: Any) -> None:
|
| 106 |
"""Turn the device on."""
|
| 107 |
await self.coordinator.api.set_switch_state(
|
| 108 |
self._dev_id,
|
| 109 |
-
self.device.get(
|
| 110 |
self.entity_description.key,
|
| 111 |
"on",
|
| 112 |
-
)
|
| 113 |
|
| 114 |
@plugwise_command
|
| 115 |
-
@override
|
| 116 |
async def async_turn_off(self, **kwargs: Any) -> None:
|
| 117 |
"""Turn the device off."""
|
| 118 |
await self.coordinator.api.set_switch_state(
|
| 119 |
self._dev_id,
|
| 120 |
-
self.device.get(
|
| 121 |
self.entity_description.key,
|
| 122 |
"off",
|
| 123 |
-
)
|
|
|
|
| 1 |
"""Plugwise Switch component for HomeAssistant."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
+
from typing import Any
|
| 5 |
|
| 6 |
from plugwise.constants import SwitchType
|
| 7 |
|
|
|
|
| 14 |
from homeassistant.core import HomeAssistant, callback
|
| 15 |
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 16 |
|
| 17 |
+
from .const import (
|
| 18 |
+
COOLING_ENA_SWITCH,
|
| 19 |
+
LOCK,
|
| 20 |
+
LOGGER, # pw-beta
|
| 21 |
+
MEMBERS,
|
| 22 |
+
RELAY,
|
| 23 |
+
SWITCHES,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Upstream consts
|
| 27 |
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 28 |
from .entity import PlugwiseEntity
|
| 29 |
from .util import plugwise_command
|
|
|
|
| 38 |
key: SwitchType
|
| 39 |
|
| 40 |
|
| 41 |
+
# Upstream consts
|
| 42 |
+
PLUGWISE_SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
PlugwiseSwitchEntityDescription(
|
| 44 |
+
key=LOCK,
|
| 45 |
+
translation_key=LOCK,
|
| 46 |
+
device_class=SwitchDeviceClass.SWITCH,
|
| 47 |
entity_category=EntityCategory.CONFIG,
|
| 48 |
),
|
| 49 |
PlugwiseSwitchEntityDescription(
|
| 50 |
+
key=RELAY,
|
| 51 |
+
translation_key=RELAY,
|
| 52 |
device_class=SwitchDeviceClass.SWITCH,
|
| 53 |
),
|
| 54 |
PlugwiseSwitchEntityDescription(
|
| 55 |
+
key=COOLING_ENA_SWITCH,
|
| 56 |
+
translation_key=COOLING_ENA_SWITCH,
|
| 57 |
+
device_class=SwitchDeviceClass.SWITCH,
|
| 58 |
entity_category=EntityCategory.CONFIG,
|
| 59 |
),
|
| 60 |
)
|
|
|
|
| 65 |
entry: PlugwiseConfigEntry,
|
| 66 |
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 67 |
) -> None:
|
| 68 |
+
"""Set up Plugwise switches from a config entry."""
|
| 69 |
coordinator = entry.runtime_data
|
| 70 |
|
| 71 |
@callback
|
|
|
|
| 74 |
if not coordinator.new_devices:
|
| 75 |
return
|
| 76 |
|
| 77 |
+
# Upstream consts
|
| 78 |
+
# async_add_entities(
|
| 79 |
+
# PlugwiseSwitchEntity(coordinator, device_id, description)
|
| 80 |
+
# for device_id in coordinator.new_devices
|
| 81 |
+
# if (switches := coordinator.data.devices[device_id].get(SWITCHES))
|
| 82 |
+
# for description in PLUGWISE_SWITCHES
|
| 83 |
+
# if description.key in switches
|
| 84 |
+
# )
|
| 85 |
+
# pw-beta alternative for debugging
|
| 86 |
+
entities: list[PlugwiseSwitchEntity] = []
|
| 87 |
+
for device_id in coordinator.new_devices:
|
| 88 |
+
device = coordinator.data[device_id]
|
| 89 |
+
if not (switches := device.get(SWITCHES)):
|
| 90 |
+
continue
|
| 91 |
+
for description in PLUGWISE_SWITCHES:
|
| 92 |
+
if description.key not in switches:
|
| 93 |
+
continue
|
| 94 |
+
entities.append(PlugwiseSwitchEntity(coordinator, device_id, description))
|
| 95 |
+
LOGGER.debug(
|
| 96 |
+
"Add %s %s switch", device["name"], description.translation_key
|
| 97 |
+
)
|
| 98 |
+
async_add_entities(entities)
|
| 99 |
|
| 100 |
_add_entities()
|
| 101 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
|
|
| 114 |
) -> None:
|
| 115 |
"""Set up the Plugwise API."""
|
| 116 |
super().__init__(coordinator, device_id)
|
|
|
|
| 117 |
self.entity_description = description
|
| 118 |
+
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 119 |
|
| 120 |
@property
|
| 121 |
+
def is_on(self) -> bool | None:
|
|
|
|
| 122 |
"""Return True if entity is on."""
|
| 123 |
+
return self.device.get(SWITCHES, {}).get(self.entity_description.key) # Upstream const
|
| 124 |
|
| 125 |
@plugwise_command
|
|
|
|
| 126 |
async def async_turn_on(self, **kwargs: Any) -> None:
|
| 127 |
"""Turn the device on."""
|
| 128 |
await self.coordinator.api.set_switch_state(
|
| 129 |
self._dev_id,
|
| 130 |
+
self.device.get(MEMBERS),
|
| 131 |
self.entity_description.key,
|
| 132 |
"on",
|
| 133 |
+
) # Upstream const
|
| 134 |
|
| 135 |
@plugwise_command
|
|
|
|
| 136 |
async def async_turn_off(self, **kwargs: Any) -> None:
|
| 137 |
"""Turn the device off."""
|
| 138 |
await self.coordinator.api.set_switch_state(
|
| 139 |
self._dev_id,
|
| 140 |
+
self.device.get(MEMBERS),
|
| 141 |
self.entity_description.key,
|
| 142 |
"off",
|
| 143 |
+
) # Upstream const
|
|
@@ -10,10 +10,15 @@
|
|
| 10 |
from .const import DOMAIN
|
| 11 |
from .entity import PlugwiseEntity
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
| 17 |
"""Decorate Plugwise calls that send commands/make changes to the device.
|
| 18 |
|
| 19 |
A decorator that wraps the passed in function, catches Plugwise errors,
|
|
@@ -21,8 +26,8 @@
|
|
| 21 |
"""
|
| 22 |
|
| 23 |
async def handler(
|
| 24 |
-
self:
|
| 25 |
-
) ->
|
| 26 |
try:
|
| 27 |
return await func(self, *args, **kwargs)
|
| 28 |
except PlugwiseException as err:
|
|
|
|
| 10 |
from .const import DOMAIN
|
| 11 |
from .entity import PlugwiseEntity
|
| 12 |
|
| 13 |
+
# For reference:
|
| 14 |
+
# PlugwiseEntityT = TypeVar("PlugwiseEntityT", bound=PlugwiseEntity)
|
| 15 |
+
# R = TypeVar("_R")
|
| 16 |
+
# P = ParamSpec("_P")
|
| 17 |
|
| 18 |
+
|
| 19 |
+
def plugwise_command[PlugwiseEntityT: PlugwiseEntity, **P, R](
|
| 20 |
+
func: Callable[Concatenate[PlugwiseEntityT, P], Awaitable[R]],
|
| 21 |
+
) -> Callable[Concatenate[PlugwiseEntityT, P], Coroutine[Any, Any, R]]:
|
| 22 |
"""Decorate Plugwise calls that send commands/make changes to the device.
|
| 23 |
|
| 24 |
A decorator that wraps the passed in function, catches Plugwise errors,
|
|
|
|
| 26 |
"""
|
| 27 |
|
| 28 |
async def handler(
|
| 29 |
+
self: PlugwiseEntityT, *args: P.args, **kwargs: P.kwargs
|
| 30 |
+
) -> R:
|
| 31 |
try:
|
| 32 |
return await func(self, *args, **kwargs)
|
| 33 |
except PlugwiseException as err:
|