|
@@ -1,119 +1,70 @@
|
|
| 1 |
"""Plugwise platform for Home Assistant Core."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
-
from plugwise.exceptions import PlugwiseError
|
| 7 |
-
import voluptuous as vol
|
| 8 |
-
|
| 9 |
-
from homeassistant.config_entries import ConfigEntry
|
| 10 |
from homeassistant.const import Platform
|
| 11 |
-
from homeassistant.core import HomeAssistant,
|
| 12 |
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
| 13 |
|
| 14 |
-
from .const import
|
| 15 |
-
|
| 16 |
-
COORDINATOR,
|
| 17 |
-
DOMAIN,
|
| 18 |
-
LOGGER,
|
| 19 |
-
PLATFORMS,
|
| 20 |
-
SERVICE_DELETE,
|
| 21 |
-
UNDO_UPDATE_LISTENER,
|
| 22 |
-
)
|
| 23 |
-
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 24 |
|
| 25 |
|
| 26 |
-
async def async_setup_entry(hass: HomeAssistant, entry:
|
| 27 |
-
"""Set up
|
| 28 |
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
| 29 |
|
| 30 |
-
|
| 31 |
-
if (
|
| 32 |
-
custom_refresh := entry.options.get(CONF_REFRESH_INTERVAL)
|
| 33 |
-
) is not None: # pragma: no cover
|
| 34 |
-
cooldown = custom_refresh
|
| 35 |
-
LOGGER.debug("DUC cooldown interval: %s", cooldown)
|
| 36 |
-
|
| 37 |
-
coordinator = PlugwiseDataUpdateCoordinator(
|
| 38 |
-
hass, entry, cooldown
|
| 39 |
-
) # pw-beta - cooldown, update_interval as extra
|
| 40 |
await coordinator.async_config_entry_first_refresh()
|
| 41 |
-
# Migrate a changed sensor unique_id
|
| 42 |
migrate_sensor_entities(hass, coordinator)
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
| 47 |
-
COORDINATOR: coordinator, # pw-beta
|
| 48 |
-
UNDO_UPDATE_LISTENER: undo_listener, # pw-beta
|
| 49 |
-
}
|
| 50 |
|
| 51 |
device_registry = dr.async_get(hass)
|
| 52 |
device_registry.async_get_or_create(
|
| 53 |
config_entry_id=entry.entry_id,
|
| 54 |
identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
|
| 55 |
manufacturer="Plugwise",
|
| 56 |
-
model=coordinator.api.
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
async def delete_notification(
|
| 62 |
-
call: ServiceCall,
|
| 63 |
-
) -> None: # pragma: no cover # pw-beta: HA service - delete_notification
|
| 64 |
-
"""Service: delete the Plugwise Notification."""
|
| 65 |
-
LOGGER.debug(
|
| 66 |
-
"Service delete PW Notification called for %s",
|
| 67 |
-
coordinator.api.smile_name,
|
| 68 |
-
)
|
| 69 |
-
try:
|
| 70 |
-
await coordinator.api.delete_notification()
|
| 71 |
-
LOGGER.debug("PW Notification deleted")
|
| 72 |
-
except PlugwiseError:
|
| 73 |
-
LOGGER.debug(
|
| 74 |
-
"Failed to delete the Plugwise Notification for %s",
|
| 75 |
-
coordinator.api.smile_name,
|
| 76 |
-
)
|
| 77 |
|
| 78 |
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
| 79 |
|
| 80 |
-
for component in PLATFORMS: # pw-beta
|
| 81 |
-
if component == Platform.BINARY_SENSOR:
|
| 82 |
-
hass.services.async_register(
|
| 83 |
-
DOMAIN, SERVICE_DELETE, delete_notification, schema=vol.Schema({})
|
| 84 |
-
)
|
| 85 |
-
|
| 86 |
return True
|
| 87 |
|
| 88 |
|
| 89 |
-
async def
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
"""Handle options update."""
|
| 93 |
-
await hass.config_entries.async_reload(entry.entry_id)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
| 97 |
-
"""Unload a config entry."""
|
| 98 |
-
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
| 99 |
-
hass.data[DOMAIN].pop(entry.entry_id)
|
| 100 |
-
return unload_ok
|
| 101 |
|
| 102 |
|
| 103 |
@callback
|
| 104 |
def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
|
| 105 |
"""Migrate Plugwise entity entries.
|
| 106 |
|
| 107 |
-
|
| 108 |
"""
|
| 109 |
-
if entry.domain == Platform.
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
if entry.domain == Platform.SENSOR and entry.unique_id.endswith(
|
| 112 |
"-relative_humidity"
|
| 113 |
):
|
| 114 |
return {
|
| 115 |
"new_unique_id": entry.unique_id.replace("-relative_humidity", "-humidity")
|
| 116 |
}
|
|
|
|
|
|
|
| 117 |
|
| 118 |
# No migration needed
|
| 119 |
return None
|
|
@@ -126,10 +77,10 @@
|
|
| 126 |
"""Migrate Sensors if needed."""
|
| 127 |
ent_reg = er.async_get(hass)
|
| 128 |
|
| 129 |
-
#
|
| 130 |
# to opentherm_outdoor_air_temperature sensor
|
| 131 |
-
for device_id, device in coordinator.data.
|
| 132 |
-
if device[
|
| 133 |
continue
|
| 134 |
|
| 135 |
old_unique_id = f"{device_id}-outdoor_temperature"
|
|
@@ -137,4 +88,10 @@
|
|
| 137 |
Platform.SENSOR, DOMAIN, old_unique_id
|
| 138 |
):
|
| 139 |
new_unique_id = f"{device_id}-outdoor_air_temperature"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
|
|
|
| 1 |
"""Plugwise platform for Home Assistant Core."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from typing import Any
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from homeassistant.const import Platform
|
| 8 |
+
from homeassistant.core import HomeAssistant, callback
|
| 9 |
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
| 10 |
|
| 11 |
+
from .const import DEV_CLASS, DOMAIN, LOGGER, PLATFORMS
|
| 12 |
+
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
+
async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
| 16 |
+
"""Set up Plugwise components from a config entry."""
|
| 17 |
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
| 18 |
|
| 19 |
+
coordinator = PlugwiseDataUpdateCoordinator(hass, entry)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
await coordinator.async_config_entry_first_refresh()
|
|
|
|
| 21 |
migrate_sensor_entities(hass, coordinator)
|
| 22 |
|
| 23 |
+
entry.runtime_data = coordinator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
device_registry = dr.async_get(hass)
|
| 26 |
device_registry.async_get_or_create(
|
| 27 |
config_entry_id=entry.entry_id,
|
| 28 |
identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
|
| 29 |
manufacturer="Plugwise",
|
| 30 |
+
model=coordinator.api.smile.model,
|
| 31 |
+
model_id=coordinator.api.smile.model_id,
|
| 32 |
+
name=coordinator.api.smile.name,
|
| 33 |
+
sw_version=str(coordinator.api.smile.version),
|
| 34 |
+
) # required for adding the entity-less P1 Gateway
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
return True
|
| 39 |
|
| 40 |
|
| 41 |
+
async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
| 42 |
+
"""Unload the Plugwise components."""
|
| 43 |
+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
@callback
|
| 47 |
def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
|
| 48 |
"""Migrate Plugwise entity entries.
|
| 49 |
|
| 50 |
+
Migrates old unique ID's from old binary_sensors and switches to the new unique ID's.
|
| 51 |
"""
|
| 52 |
+
if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
|
| 53 |
+
"-slave_boiler_state"
|
| 54 |
+
):
|
| 55 |
+
return {
|
| 56 |
+
"new_unique_id": entry.unique_id.replace(
|
| 57 |
+
"-slave_boiler_state", "-secondary_boiler_state"
|
| 58 |
+
)
|
| 59 |
+
}
|
| 60 |
if entry.domain == Platform.SENSOR and entry.unique_id.endswith(
|
| 61 |
"-relative_humidity"
|
| 62 |
):
|
| 63 |
return {
|
| 64 |
"new_unique_id": entry.unique_id.replace("-relative_humidity", "-humidity")
|
| 65 |
}
|
| 66 |
+
if entry.domain == Platform.SWITCH and entry.unique_id.endswith("-plug"):
|
| 67 |
+
return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")}
|
| 68 |
|
| 69 |
# No migration needed
|
| 70 |
return None
|
|
|
|
| 77 |
"""Migrate Sensors if needed."""
|
| 78 |
ent_reg = er.async_get(hass)
|
| 79 |
|
| 80 |
+
# Migrating opentherm_outdoor_temperature
|
| 81 |
# to opentherm_outdoor_air_temperature sensor
|
| 82 |
+
for device_id, device in coordinator.data.items():
|
| 83 |
+
if device[DEV_CLASS] != "heater_central":
|
| 84 |
continue
|
| 85 |
|
| 86 |
old_unique_id = f"{device_id}-outdoor_temperature"
|
|
|
|
| 88 |
Platform.SENSOR, DOMAIN, old_unique_id
|
| 89 |
):
|
| 90 |
new_unique_id = f"{device_id}-outdoor_air_temperature"
|
| 91 |
+
LOGGER.debug(
|
| 92 |
+
"Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
|
| 93 |
+
entity_id,
|
| 94 |
+
old_unique_id,
|
| 95 |
+
new_unique_id,
|
| 96 |
+
)
|
| 97 |
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
"""Plugwise Binary Sensor component for Home Assistant."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
from collections.abc import Mapping
|
|
@@ -8,88 +9,74 @@
|
|
| 8 |
from plugwise.constants import BinarySensorType
|
| 9 |
|
| 10 |
from homeassistant.components.binary_sensor import (
|
|
|
|
| 11 |
BinarySensorEntity,
|
| 12 |
BinarySensorEntityDescription,
|
| 13 |
)
|
| 14 |
-
from homeassistant.config_entries import ConfigEntry
|
| 15 |
from homeassistant.const import EntityCategory
|
| 16 |
-
from homeassistant.core import HomeAssistant
|
| 17 |
-
from homeassistant.helpers.entity_platform import
|
| 18 |
|
| 19 |
-
from .
|
| 20 |
-
COORDINATOR, # pw-beta
|
| 21 |
-
DOMAIN,
|
| 22 |
-
LOGGER,
|
| 23 |
-
SEVERITIES,
|
| 24 |
-
)
|
| 25 |
-
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 26 |
from .entity import PlugwiseEntity
|
| 27 |
|
|
|
|
|
|
|
|
|
|
| 28 |
PARALLEL_UPDATES = 0
|
| 29 |
|
| 30 |
|
| 31 |
-
@dataclass
|
| 32 |
class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription):
|
| 33 |
"""Describes a Plugwise binary sensor entity."""
|
| 34 |
|
| 35 |
key: BinarySensorType
|
| 36 |
-
icon_off: str | None = None
|
| 37 |
|
| 38 |
|
| 39 |
BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
|
| 40 |
PlugwiseBinarySensorEntityDescription(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
key="compressor_state",
|
| 42 |
translation_key="compressor_state",
|
| 43 |
-
icon="mdi:hvac",
|
| 44 |
-
icon_off="mdi:hvac-off",
|
| 45 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 46 |
),
|
| 47 |
PlugwiseBinarySensorEntityDescription(
|
| 48 |
key="cooling_enabled",
|
| 49 |
translation_key="cooling_enabled",
|
| 50 |
-
icon="mdi:snowflake-thermometer",
|
| 51 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 52 |
),
|
| 53 |
PlugwiseBinarySensorEntityDescription(
|
| 54 |
key="dhw_state",
|
| 55 |
translation_key="dhw_state",
|
| 56 |
-
icon="mdi:water-pump",
|
| 57 |
-
icon_off="mdi:water-pump-off",
|
| 58 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 59 |
),
|
| 60 |
PlugwiseBinarySensorEntityDescription(
|
| 61 |
key="flame_state",
|
| 62 |
translation_key="flame_state",
|
| 63 |
-
icon="mdi:fire",
|
| 64 |
-
icon_off="mdi:fire-off",
|
| 65 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 66 |
),
|
| 67 |
PlugwiseBinarySensorEntityDescription(
|
| 68 |
key="heating_state",
|
| 69 |
translation_key="heating_state",
|
| 70 |
-
icon="mdi:radiator",
|
| 71 |
-
icon_off="mdi:radiator-off",
|
| 72 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 73 |
),
|
| 74 |
PlugwiseBinarySensorEntityDescription(
|
| 75 |
key="cooling_state",
|
| 76 |
translation_key="cooling_state",
|
| 77 |
-
icon="mdi:snowflake",
|
| 78 |
-
icon_off="mdi:snowflake-off",
|
| 79 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 80 |
),
|
| 81 |
PlugwiseBinarySensorEntityDescription(
|
| 82 |
-
key="
|
| 83 |
-
translation_key="
|
| 84 |
-
icon="mdi:fire",
|
| 85 |
-
icon_off="mdi:circle-off-outline",
|
| 86 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 87 |
),
|
| 88 |
PlugwiseBinarySensorEntityDescription(
|
| 89 |
key="plugwise_notification",
|
| 90 |
translation_key="plugwise_notification",
|
| 91 |
-
icon="mdi:mailbox-up-outline",
|
| 92 |
-
icon_off="mdi:mailbox-outline",
|
| 93 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 94 |
),
|
| 95 |
)
|
|
@@ -97,33 +84,28 @@
|
|
| 97 |
|
| 98 |
async def async_setup_entry(
|
| 99 |
hass: HomeAssistant,
|
| 100 |
-
|
| 101 |
-
async_add_entities:
|
| 102 |
) -> None:
|
| 103 |
-
"""Set up the
|
| 104 |
-
coordinator
|
| 105 |
-
config_entry.entry_id
|
| 106 |
-
][COORDINATOR]
|
| 107 |
-
|
| 108 |
-
entities: list[PlugwiseBinarySensorEntity] = []
|
| 109 |
-
for device_id, device in coordinator.data.devices.items():
|
| 110 |
-
if not (binary_sensors := device.get("binary_sensors")):
|
| 111 |
-
continue
|
| 112 |
-
for description in BINARY_SENSORS:
|
| 113 |
-
if description.key not in binary_sensors:
|
| 114 |
-
continue
|
| 115 |
-
entities.append(
|
| 116 |
-
PlugwiseBinarySensorEntity(
|
| 117 |
-
coordinator,
|
| 118 |
-
device_id,
|
| 119 |
-
description,
|
| 120 |
-
)
|
| 121 |
-
)
|
| 122 |
-
LOGGER.debug(
|
| 123 |
-
"Add %s %s binary sensor", device["name"], description.translation_key
|
| 124 |
-
)
|
| 125 |
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
|
| 129 |
class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
|
|
@@ -141,53 +123,26 @@
|
|
| 141 |
super().__init__(coordinator, device_id)
|
| 142 |
self.entity_description = description
|
| 143 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 144 |
-
self._notification: dict[str, str] = {} # pw-beta
|
| 145 |
|
| 146 |
@property
|
| 147 |
def is_on(self) -> bool:
|
| 148 |
"""Return true if the binary sensor is on."""
|
| 149 |
-
# pw-beta: show Plugwise notifications as HA persistent notifications
|
| 150 |
-
if self._notification:
|
| 151 |
-
for notify_id, message in self._notification.items():
|
| 152 |
-
self.hass.components.persistent_notification.async_create(
|
| 153 |
-
message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
|
| 154 |
-
)
|
| 155 |
-
|
| 156 |
-
# return self.device["binary_sensors"][self.entity_description.key] # type: ignore [literal-required]
|
| 157 |
return self.device["binary_sensors"][self.entity_description.key]
|
| 158 |
|
| 159 |
@property
|
| 160 |
-
def icon(self) -> str | None:
|
| 161 |
-
"""Return the icon to use in the frontend, if any."""
|
| 162 |
-
if (icon_off := self.entity_description.icon_off) and self.is_on is False:
|
| 163 |
-
return icon_off
|
| 164 |
-
return self.entity_description.icon
|
| 165 |
-
|
| 166 |
-
@property
|
| 167 |
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
| 168 |
"""Return entity specific state attributes."""
|
| 169 |
if self.entity_description.key != "plugwise_notification":
|
| 170 |
return None
|
| 171 |
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
if notify := self.coordinator.data.gateway["notifications"]:
|
| 177 |
-
for notify_id, details in notify.items(): # pw-beta uses notify_id
|
| 178 |
for msg_type, msg in details.items():
|
| 179 |
msg_type = msg_type.lower()
|
| 180 |
if msg_type not in SEVERITIES:
|
| 181 |
-
msg_type = "other"
|
| 182 |
-
|
| 183 |
-
if (
|
| 184 |
-
f"{msg_type}_msg" not in attrs
|
| 185 |
-
): # pw-beta Re-evaluate against Core
|
| 186 |
-
attrs[f"{msg_type}_msg"] = []
|
| 187 |
attrs[f"{msg_type}_msg"].append(msg)
|
| 188 |
|
| 189 |
-
self._notification[
|
| 190 |
-
notify_id
|
| 191 |
-
] = f"{msg_type.title()}: {msg}" # pw-beta
|
| 192 |
-
|
| 193 |
return attrs
|
|
|
|
| 1 |
"""Plugwise Binary Sensor component for Home Assistant."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from collections.abc import Mapping
|
|
|
|
| 9 |
from plugwise.constants import BinarySensorType
|
| 10 |
|
| 11 |
from homeassistant.components.binary_sensor import (
|
| 12 |
+
BinarySensorDeviceClass,
|
| 13 |
BinarySensorEntity,
|
| 14 |
BinarySensorEntityDescription,
|
| 15 |
)
|
|
|
|
| 16 |
from homeassistant.const import EntityCategory
|
| 17 |
+
from homeassistant.core import HomeAssistant, callback
|
| 18 |
+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 19 |
|
| 20 |
+
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
from .entity import PlugwiseEntity
|
| 22 |
|
| 23 |
+
SEVERITIES = ["other", "info", "warning", "error"]
|
| 24 |
+
|
| 25 |
+
# Coordinator is used to centralize the data updates
|
| 26 |
PARALLEL_UPDATES = 0
|
| 27 |
|
| 28 |
|
| 29 |
+
@dataclass(frozen=True)
|
| 30 |
class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription):
|
| 31 |
"""Describes a Plugwise binary sensor entity."""
|
| 32 |
|
| 33 |
key: BinarySensorType
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
|
| 37 |
PlugwiseBinarySensorEntityDescription(
|
| 38 |
+
key="low_battery",
|
| 39 |
+
device_class=BinarySensorDeviceClass.BATTERY,
|
| 40 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
| 41 |
+
),
|
| 42 |
+
PlugwiseBinarySensorEntityDescription(
|
| 43 |
key="compressor_state",
|
| 44 |
translation_key="compressor_state",
|
|
|
|
|
|
|
| 45 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 46 |
),
|
| 47 |
PlugwiseBinarySensorEntityDescription(
|
| 48 |
key="cooling_enabled",
|
| 49 |
translation_key="cooling_enabled",
|
|
|
|
| 50 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 51 |
),
|
| 52 |
PlugwiseBinarySensorEntityDescription(
|
| 53 |
key="dhw_state",
|
| 54 |
translation_key="dhw_state",
|
|
|
|
|
|
|
| 55 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 56 |
),
|
| 57 |
PlugwiseBinarySensorEntityDescription(
|
| 58 |
key="flame_state",
|
| 59 |
translation_key="flame_state",
|
|
|
|
|
|
|
| 60 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 61 |
),
|
| 62 |
PlugwiseBinarySensorEntityDescription(
|
| 63 |
key="heating_state",
|
| 64 |
translation_key="heating_state",
|
|
|
|
|
|
|
| 65 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 66 |
),
|
| 67 |
PlugwiseBinarySensorEntityDescription(
|
| 68 |
key="cooling_state",
|
| 69 |
translation_key="cooling_state",
|
|
|
|
|
|
|
| 70 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 71 |
),
|
| 72 |
PlugwiseBinarySensorEntityDescription(
|
| 73 |
+
key="secondary_boiler_state",
|
| 74 |
+
translation_key="secondary_boiler_state",
|
|
|
|
|
|
|
| 75 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 76 |
),
|
| 77 |
PlugwiseBinarySensorEntityDescription(
|
| 78 |
key="plugwise_notification",
|
| 79 |
translation_key="plugwise_notification",
|
|
|
|
|
|
|
| 80 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 81 |
),
|
| 82 |
)
|
|
|
|
| 84 |
|
| 85 |
async def async_setup_entry(
|
| 86 |
hass: HomeAssistant,
|
| 87 |
+
entry: PlugwiseConfigEntry,
|
| 88 |
+
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 89 |
) -> None:
|
| 90 |
+
"""Set up the Smile binary_sensors from a config entry."""
|
| 91 |
+
coordinator = entry.runtime_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
@callback
|
| 94 |
+
def _add_entities() -> None:
|
| 95 |
+
"""Add Entities."""
|
| 96 |
+
if not coordinator.new_devices:
|
| 97 |
+
return
|
| 98 |
+
|
| 99 |
+
async_add_entities(
|
| 100 |
+
PlugwiseBinarySensorEntity(coordinator, device_id, description)
|
| 101 |
+
for device_id in coordinator.new_devices
|
| 102 |
+
if (binary_sensors := coordinator.data[device_id].get("binary_sensors"))
|
| 103 |
+
for description in BINARY_SENSORS
|
| 104 |
+
if description.key in binary_sensors
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
_add_entities()
|
| 108 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 109 |
|
| 110 |
|
| 111 |
class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
|
|
|
|
| 123 |
super().__init__(coordinator, device_id)
|
| 124 |
self.entity_description = description
|
| 125 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
|
|
|
| 126 |
|
| 127 |
@property
|
| 128 |
def is_on(self) -> bool:
|
| 129 |
"""Return true if the binary sensor is on."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
return self.device["binary_sensors"][self.entity_description.key]
|
| 131 |
|
| 132 |
@property
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
| 134 |
"""Return entity specific state attributes."""
|
| 135 |
if self.entity_description.key != "plugwise_notification":
|
| 136 |
return None
|
| 137 |
|
| 138 |
+
attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES}
|
| 139 |
+
gateway_id = self.coordinator.api.gateway_id
|
| 140 |
+
if notify := self.coordinator.data[gateway_id]["notifications"]:
|
| 141 |
+
for details in notify.values():
|
|
|
|
|
|
|
| 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
|
|
@@ -1,6 +1,8 @@
|
|
| 1 |
"""Plugwise Climate component for Home Assistant."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
|
|
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
from homeassistant.components.climate import (
|
|
@@ -12,94 +14,133 @@
|
|
| 12 |
HVACAction,
|
| 13 |
HVACMode,
|
| 14 |
)
|
| 15 |
-
from homeassistant.
|
| 16 |
-
|
| 17 |
-
PRESET_HOME, # pw-beta homekit emulation
|
| 18 |
-
)
|
| 19 |
-
from homeassistant.config_entries import ConfigEntry
|
| 20 |
-
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
| 21 |
-
from homeassistant.core import HomeAssistant
|
| 22 |
from homeassistant.exceptions import HomeAssistantError
|
| 23 |
-
from homeassistant.helpers.entity_platform import
|
|
|
|
| 24 |
|
| 25 |
-
from .const import
|
| 26 |
-
|
| 27 |
-
COORDINATOR, # pw-beta
|
| 28 |
-
DOMAIN,
|
| 29 |
-
MASTER_THERMOSTATS,
|
| 30 |
-
)
|
| 31 |
-
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 32 |
from .entity import PlugwiseEntity
|
| 33 |
from .util import plugwise_command
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
async def async_setup_entry(
|
| 37 |
hass: HomeAssistant,
|
| 38 |
-
|
| 39 |
-
async_add_entities:
|
| 40 |
) -> None:
|
| 41 |
"""Set up the Smile Thermostats from a config entry."""
|
| 42 |
-
coordinator
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
|
|
|
|
| 60 |
"""Representation of a Plugwise thermostat."""
|
| 61 |
|
| 62 |
-
_attr_has_entity_name = True
|
| 63 |
_attr_name = None
|
| 64 |
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
| 65 |
_attr_translation_key = DOMAIN
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
def __init__(
|
| 68 |
self,
|
| 69 |
coordinator: PlugwiseDataUpdateCoordinator,
|
| 70 |
device_id: str,
|
| 71 |
-
homekit_enabled: bool, # pw-beta homekit emulation
|
| 72 |
) -> None:
|
| 73 |
"""Set up the Plugwise API."""
|
| 74 |
super().__init__(coordinator, device_id)
|
| 75 |
-
self._homekit_enabled = homekit_enabled # pw-beta homekit emulation
|
| 76 |
-
self._homekit_mode: str | None = None # pw-beta homekit emulation
|
| 77 |
self._attr_unique_id = f"{device_id}-climate"
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
# Determine supported features
|
| 80 |
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
| 81 |
-
if
|
|
|
|
|
|
|
|
|
|
| 82 |
self._attr_supported_features = (
|
| 83 |
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
| 84 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
if presets := self.device.get("preset_modes"):
|
| 86 |
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
| 87 |
self._attr_preset_modes = presets
|
| 88 |
|
| 89 |
-
# Determine hvac modes and current hvac mode
|
| 90 |
-
self._attr_hvac_modes = [HVACMode.HEAT]
|
| 91 |
-
if self.coordinator.data.gateway["cooling_present"]:
|
| 92 |
-
self._attr_hvac_modes = [HVACMode.HEAT_COOL]
|
| 93 |
-
if self.device["available_schedules"] != ["None"]:
|
| 94 |
-
self._attr_hvac_modes.append(HVACMode.AUTO)
|
| 95 |
-
if self._homekit_enabled: # pw-beta homekit emulation
|
| 96 |
-
self._attr_hvac_modes.append(HVACMode.OFF) # pragma: no cover
|
| 97 |
-
|
| 98 |
self._attr_min_temp = self.device["thermostat"]["lower_bound"]
|
| 99 |
-
self._attr_max_temp = self.device["thermostat"]["upper_bound"]
|
| 100 |
-
#
|
| 101 |
self._attr_target_temperature_step = max(
|
| 102 |
-
self.device["thermostat"]["resolution"], 0.
|
| 103 |
)
|
| 104 |
|
| 105 |
@property
|
|
@@ -108,6 +149,14 @@
|
|
| 108 |
return self.device["sensors"]["temperature"]
|
| 109 |
|
| 110 |
@property
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
def target_temperature(self) -> float:
|
| 112 |
"""Return the temperature we try to reach.
|
| 113 |
|
|
@@ -134,44 +183,59 @@
|
|
| 134 |
|
| 135 |
@property
|
| 136 |
def hvac_mode(self) -> HVACMode:
|
| 137 |
-
"""Return HVAC operation ie. auto, heat, heat_cool, or off mode."""
|
| 138 |
if (
|
| 139 |
-
mode := self.device
|
| 140 |
-
) is None or mode not in self.hvac_modes:
|
| 141 |
-
return HVACMode.HEAT
|
| 142 |
-
# pw-beta homekit emulation
|
| 143 |
-
if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
|
| 144 |
-
mode = HVACMode.OFF # pragma: no cover
|
| 145 |
-
|
| 146 |
return HVACMode(mode)
|
| 147 |
|
| 148 |
@property
|
| 149 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
"""Return the current running hvac operation if supported."""
|
| 151 |
-
#
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
heater_data = self.coordinator.data.devices[heater]
|
| 164 |
-
if heater_data["binary_sensors"]["heating_state"]:
|
| 165 |
-
return HVACAction.HEATING
|
| 166 |
-
if heater_data["binary_sensors"].get("cooling_state", False):
|
| 167 |
-
return HVACAction.COOLING
|
| 168 |
|
| 169 |
return HVACAction.IDLE
|
| 170 |
|
| 171 |
@property
|
| 172 |
def preset_mode(self) -> str | None:
|
| 173 |
"""Return the current preset mode."""
|
| 174 |
-
return self.device
|
| 175 |
|
| 176 |
@plugwise_command
|
| 177 |
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
@@ -184,41 +248,47 @@
|
|
| 184 |
if ATTR_TARGET_TEMP_LOW in kwargs:
|
| 185 |
data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
| 186 |
|
| 187 |
-
for temperature in data.values():
|
| 188 |
-
if temperature is None or not (
|
| 189 |
-
self._attr_min_temp <= temperature <= self._attr_max_temp
|
| 190 |
-
):
|
| 191 |
-
raise ValueError("Invalid temperature change requested")
|
| 192 |
-
|
| 193 |
if mode := kwargs.get(ATTR_HVAC_MODE):
|
| 194 |
await self.async_set_hvac_mode(mode)
|
| 195 |
|
| 196 |
-
await self.coordinator.api.set_temperature(self.
|
| 197 |
|
| 198 |
@plugwise_command
|
| 199 |
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
| 200 |
"""Set the hvac mode."""
|
| 201 |
-
if hvac_mode
|
| 202 |
-
|
| 203 |
|
| 204 |
-
|
| 205 |
-
self.
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
@plugwise_command
|
| 222 |
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
| 223 |
"""Set the preset mode."""
|
| 224 |
-
await self.coordinator.api.set_preset(self.
|
|
|
|
| 1 |
"""Plugwise Climate component for Home Assistant."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
from typing import Any
|
| 7 |
|
| 8 |
from homeassistant.components.climate import (
|
|
|
|
| 14 |
HVACAction,
|
| 15 |
HVACMode,
|
| 16 |
)
|
| 17 |
+
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON, UnitOfTemperature
|
| 18 |
+
from homeassistant.core import HomeAssistant, callback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
from homeassistant.exceptions import HomeAssistantError
|
| 20 |
+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 21 |
+
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
|
| 22 |
|
| 23 |
+
from .const import DOMAIN, MASTER_THERMOSTATS
|
| 24 |
+
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
from .entity import PlugwiseEntity
|
| 26 |
from .util import plugwise_command
|
| 27 |
|
| 28 |
+
ERROR_NO_SCHEDULE = "set_schedule_first"
|
| 29 |
+
PARALLEL_UPDATES = 0
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class PlugwiseClimateExtraStoredData(ExtraStoredData):
|
| 34 |
+
"""Object to hold extra stored data."""
|
| 35 |
+
|
| 36 |
+
last_active_schedule: str | None
|
| 37 |
+
previous_action_mode: str | None
|
| 38 |
+
|
| 39 |
+
def as_dict(self) -> dict[str, Any]:
|
| 40 |
+
"""Return a dict representation of the text data."""
|
| 41 |
+
return {
|
| 42 |
+
"last_active_schedule": self.last_active_schedule,
|
| 43 |
+
"previous_action_mode": self.previous_action_mode,
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
@classmethod
|
| 47 |
+
def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData:
|
| 48 |
+
"""Initialize a stored data object from a dict."""
|
| 49 |
+
return cls(
|
| 50 |
+
last_active_schedule=restored.get("last_active_schedule"),
|
| 51 |
+
previous_action_mode=restored.get("previous_action_mode"),
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
|
| 55 |
async def async_setup_entry(
|
| 56 |
hass: HomeAssistant,
|
| 57 |
+
entry: PlugwiseConfigEntry,
|
| 58 |
+
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 59 |
) -> None:
|
| 60 |
"""Set up the Smile Thermostats from a config entry."""
|
| 61 |
+
coordinator = entry.runtime_data
|
| 62 |
+
|
| 63 |
+
@callback
|
| 64 |
+
def _add_entities() -> None:
|
| 65 |
+
"""Add Entities."""
|
| 66 |
+
if not coordinator.new_devices:
|
| 67 |
+
return
|
| 68 |
+
|
| 69 |
+
if coordinator.api.smile.name == "Adam":
|
| 70 |
+
async_add_entities(
|
| 71 |
+
PlugwiseClimateEntity(coordinator, device_id)
|
| 72 |
+
for device_id in coordinator.new_devices
|
| 73 |
+
if coordinator.data[device_id]["dev_class"] == "climate"
|
| 74 |
+
)
|
| 75 |
+
else:
|
| 76 |
+
async_add_entities(
|
| 77 |
+
PlugwiseClimateEntity(coordinator, device_id)
|
| 78 |
+
for device_id in coordinator.new_devices
|
| 79 |
+
if coordinator.data[device_id]["dev_class"] in MASTER_THERMOSTATS
|
| 80 |
+
)
|
| 81 |
|
| 82 |
+
_add_entities()
|
| 83 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 84 |
|
| 85 |
+
|
| 86 |
+
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
|
| 87 |
"""Representation of a Plugwise thermostat."""
|
| 88 |
|
|
|
|
| 89 |
_attr_name = None
|
| 90 |
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
| 91 |
_attr_translation_key = DOMAIN
|
| 92 |
|
| 93 |
+
_last_active_schedule: str | None = None
|
| 94 |
+
_previous_action_mode: str | None = HVACAction.HEATING.value
|
| 95 |
+
|
| 96 |
+
async def async_added_to_hass(self) -> None:
|
| 97 |
+
"""Run when entity about to be added."""
|
| 98 |
+
await super().async_added_to_hass()
|
| 99 |
+
|
| 100 |
+
if extra_data := await self.async_get_last_extra_data():
|
| 101 |
+
plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
|
| 102 |
+
extra_data.as_dict()
|
| 103 |
+
)
|
| 104 |
+
self._last_active_schedule = plugwise_extra_data.last_active_schedule
|
| 105 |
+
self._previous_action_mode = plugwise_extra_data.previous_action_mode
|
| 106 |
+
|
| 107 |
def __init__(
|
| 108 |
self,
|
| 109 |
coordinator: PlugwiseDataUpdateCoordinator,
|
| 110 |
device_id: str,
|
|
|
|
| 111 |
) -> None:
|
| 112 |
"""Set up the Plugwise API."""
|
| 113 |
super().__init__(coordinator, device_id)
|
|
|
|
|
|
|
| 114 |
self._attr_unique_id = f"{device_id}-climate"
|
| 115 |
|
| 116 |
+
gateway_id: str = coordinator.api.gateway_id
|
| 117 |
+
self._gateway_data = coordinator.data[gateway_id]
|
| 118 |
+
self._location = device_id
|
| 119 |
+
if (location := self.device.get("location")) is not None:
|
| 120 |
+
self._location = location
|
| 121 |
+
|
| 122 |
# Determine supported features
|
| 123 |
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
| 124 |
+
if (
|
| 125 |
+
self.coordinator.api.cooling_present
|
| 126 |
+
and coordinator.api.smile.name != "Adam"
|
| 127 |
+
):
|
| 128 |
self._attr_supported_features = (
|
| 129 |
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
| 130 |
)
|
| 131 |
+
if HVACMode.OFF in self.hvac_modes:
|
| 132 |
+
self._attr_supported_features |= (
|
| 133 |
+
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
| 134 |
+
)
|
| 135 |
if presets := self.device.get("preset_modes"):
|
| 136 |
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
| 137 |
self._attr_preset_modes = presets
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
self._attr_min_temp = self.device["thermostat"]["lower_bound"]
|
| 140 |
+
self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
|
| 141 |
+
# Ensure we don't drop below 0.1
|
| 142 |
self._attr_target_temperature_step = max(
|
| 143 |
+
self.device["thermostat"]["resolution"], 0.1
|
| 144 |
)
|
| 145 |
|
| 146 |
@property
|
|
|
|
| 149 |
return self.device["sensors"]["temperature"]
|
| 150 |
|
| 151 |
@property
|
| 152 |
+
def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
|
| 153 |
+
"""Return text specific state data to be restored."""
|
| 154 |
+
return PlugwiseClimateExtraStoredData(
|
| 155 |
+
last_active_schedule=self._last_active_schedule,
|
| 156 |
+
previous_action_mode=self._previous_action_mode,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
@property
|
| 160 |
def target_temperature(self) -> float:
|
| 161 |
"""Return the temperature we try to reach.
|
| 162 |
|
|
|
|
| 183 |
|
| 184 |
@property
|
| 185 |
def hvac_mode(self) -> HVACMode:
|
| 186 |
+
"""Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
|
| 187 |
if (
|
| 188 |
+
mode := self.device.get("climate_mode")
|
| 189 |
+
) is None or mode not in self.hvac_modes:
|
| 190 |
+
return HVACMode.HEAT
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
return HVACMode(mode)
|
| 192 |
|
| 193 |
@property
|
| 194 |
+
def hvac_modes(self) -> list[HVACMode]:
|
| 195 |
+
"""Return a list of available HVACModes."""
|
| 196 |
+
hvac_modes: list[HVACMode] = []
|
| 197 |
+
if "regulation_modes" in self._gateway_data:
|
| 198 |
+
hvac_modes.append(HVACMode.OFF)
|
| 199 |
+
|
| 200 |
+
if self.device.get("available_schedules"):
|
| 201 |
+
hvac_modes.append(HVACMode.AUTO)
|
| 202 |
+
|
| 203 |
+
if self.coordinator.api.cooling_present:
|
| 204 |
+
if "regulation_modes" in self._gateway_data:
|
| 205 |
+
selected = self._gateway_data.get("select_regulation_mode")
|
| 206 |
+
if selected == HVACAction.COOLING.value:
|
| 207 |
+
hvac_modes.append(HVACMode.COOL)
|
| 208 |
+
if selected == HVACAction.HEATING.value:
|
| 209 |
+
hvac_modes.append(HVACMode.HEAT)
|
| 210 |
+
else:
|
| 211 |
+
hvac_modes.append(HVACMode.HEAT_COOL)
|
| 212 |
+
else:
|
| 213 |
+
hvac_modes.append(HVACMode.HEAT)
|
| 214 |
+
|
| 215 |
+
return hvac_modes
|
| 216 |
+
|
| 217 |
+
@property
|
| 218 |
+
def hvac_action(self) -> HVACAction:
|
| 219 |
"""Return the current running hvac operation if supported."""
|
| 220 |
+
# Keep track of the previous hvac_action mode.
|
| 221 |
+
# When no cooling available, _previous_action_mode is always heating
|
| 222 |
+
if (
|
| 223 |
+
"regulation_modes" in self._gateway_data
|
| 224 |
+
and HVACAction.COOLING.value in self._gateway_data["regulation_modes"]
|
| 225 |
+
):
|
| 226 |
+
mode = self._gateway_data["select_regulation_mode"]
|
| 227 |
+
if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
|
| 228 |
+
self._previous_action_mode = mode
|
| 229 |
+
|
| 230 |
+
if (action := self.device.get("control_state")) is not None:
|
| 231 |
+
return HVACAction(action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
return HVACAction.IDLE
|
| 234 |
|
| 235 |
@property
|
| 236 |
def preset_mode(self) -> str | None:
|
| 237 |
"""Return the current preset mode."""
|
| 238 |
+
return self.device.get("active_preset")
|
| 239 |
|
| 240 |
@plugwise_command
|
| 241 |
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
|
|
| 248 |
if ATTR_TARGET_TEMP_LOW in kwargs:
|
| 249 |
data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
if mode := kwargs.get(ATTR_HVAC_MODE):
|
| 252 |
await self.async_set_hvac_mode(mode)
|
| 253 |
|
| 254 |
+
await self.coordinator.api.set_temperature(self._location, data)
|
| 255 |
|
| 256 |
@plugwise_command
|
| 257 |
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
| 258 |
"""Set the hvac mode."""
|
| 259 |
+
if hvac_mode == self.hvac_mode:
|
| 260 |
+
return
|
| 261 |
|
| 262 |
+
if hvac_mode == HVACMode.OFF:
|
| 263 |
+
await self.coordinator.api.set_regulation_mode(hvac_mode.value)
|
| 264 |
+
else:
|
| 265 |
+
current = self.device.get("select_schedule")
|
| 266 |
+
desired = current
|
| 267 |
+
|
| 268 |
+
# Capture the last valid schedule
|
| 269 |
+
if desired and desired != "off":
|
| 270 |
+
self._last_active_schedule = desired
|
| 271 |
+
elif desired == "off":
|
| 272 |
+
desired = self._last_active_schedule
|
| 273 |
+
|
| 274 |
+
# Enabling HVACMode.AUTO requires a previously set schedule for saving and restoring
|
| 275 |
+
if hvac_mode == HVACMode.AUTO and not desired:
|
| 276 |
+
raise HomeAssistantError(
|
| 277 |
+
translation_domain=DOMAIN,
|
| 278 |
+
translation_key=ERROR_NO_SCHEDULE,
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
await self.coordinator.api.set_schedule_state(
|
| 282 |
+
self._location,
|
| 283 |
+
STATE_ON if hvac_mode == HVACMode.AUTO else STATE_OFF,
|
| 284 |
+
desired,
|
| 285 |
+
)
|
| 286 |
+
if self.hvac_mode == HVACMode.OFF and self._previous_action_mode:
|
| 287 |
+
await self.coordinator.api.set_regulation_mode(
|
| 288 |
+
self._previous_action_mode
|
| 289 |
+
)
|
| 290 |
|
| 291 |
@plugwise_command
|
| 292 |
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
| 293 |
"""Set the preset mode."""
|
| 294 |
+
await self.coordinator.api.set_preset(self._location, preset_mode)
|
|
@@ -1,8 +1,9 @@
|
|
| 1 |
"""Config flow for Plugwise integration."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
-
import
|
| 5 |
-
from typing import Any
|
| 6 |
|
| 7 |
from plugwise import Smile
|
| 8 |
from plugwise.exceptions import (
|
|
@@ -15,75 +16,66 @@
|
|
| 15 |
)
|
| 16 |
import voluptuous as vol
|
| 17 |
|
| 18 |
-
from homeassistant import
|
| 19 |
-
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
| 20 |
-
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
| 21 |
from homeassistant.const import (
|
|
|
|
| 22 |
CONF_BASE,
|
| 23 |
CONF_HOST,
|
| 24 |
CONF_NAME,
|
| 25 |
CONF_PASSWORD,
|
| 26 |
CONF_PORT,
|
| 27 |
-
CONF_SCAN_INTERVAL,
|
| 28 |
CONF_USERNAME,
|
| 29 |
)
|
| 30 |
-
from homeassistant.core import HomeAssistant
|
| 31 |
-
from homeassistant.data_entry_flow import FlowResult
|
| 32 |
-
from homeassistant.helpers import config_validation as cv
|
| 33 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
|
|
| 34 |
|
| 35 |
from .const import (
|
| 36 |
-
|
| 37 |
-
CONF_REFRESH_INTERVAL, # pw-beta option
|
| 38 |
-
COORDINATOR,
|
| 39 |
DEFAULT_PORT,
|
| 40 |
-
DEFAULT_SCAN_INTERVAL, # pw-beta option
|
| 41 |
DEFAULT_USERNAME,
|
| 42 |
DOMAIN,
|
| 43 |
FLOW_SMILE,
|
| 44 |
FLOW_STRETCH,
|
| 45 |
SMILE,
|
|
|
|
|
|
|
| 46 |
STRETCH,
|
| 47 |
STRETCH_USERNAME,
|
|
|
|
| 48 |
ZEROCONF_MAP,
|
| 49 |
)
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
def
|
| 53 |
-
discovery_info: ZeroconfServiceInfo | None,
|
| 54 |
-
user_input: dict[str, Any] | None,
|
| 55 |
-
) -> vol.Schema:
|
| 56 |
"""Generate base schema for gateways."""
|
|
|
|
|
|
|
| 57 |
if not discovery_info:
|
| 58 |
-
|
| 59 |
-
return vol.Schema(
|
| 60 |
-
{
|
| 61 |
-
vol.Required(CONF_PASSWORD): str,
|
| 62 |
-
vol.Required(CONF_HOST): str,
|
| 63 |
-
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
| 64 |
-
vol.Required(CONF_USERNAME, default=SMILE): vol.In(
|
| 65 |
-
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
| 66 |
-
),
|
| 67 |
-
}
|
| 68 |
-
)
|
| 69 |
-
return vol.Schema(
|
| 70 |
{
|
| 71 |
-
vol.Required(
|
| 72 |
-
vol.Required(
|
| 73 |
-
vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): int,
|
| 74 |
-
vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): vol.In(
|
| 75 |
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
| 76 |
),
|
| 77 |
}
|
| 78 |
)
|
| 79 |
|
| 80 |
-
return
|
| 81 |
|
| 82 |
|
| 83 |
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
|
| 84 |
"""Validate whether the user input allows us to connect to the gateway.
|
| 85 |
|
| 86 |
-
Data has the keys from
|
| 87 |
"""
|
| 88 |
websession = async_get_clientsession(hass, verify_ssl=False)
|
| 89 |
api = Smile(
|
|
@@ -91,24 +83,50 @@
|
|
| 91 |
password=data[CONF_PASSWORD],
|
| 92 |
port=data[CONF_PORT],
|
| 93 |
username=data[CONF_USERNAME],
|
| 94 |
-
timeout=30,
|
| 95 |
websession=websession,
|
| 96 |
)
|
| 97 |
await api.connect()
|
| 98 |
return api
|
| 99 |
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
| 102 |
"""Handle a config flow for Plugwise Smile."""
|
| 103 |
|
| 104 |
VERSION = 1
|
| 105 |
|
| 106 |
discovery_info: ZeroconfServiceInfo | None = None
|
|
|
|
| 107 |
_username: str = DEFAULT_USERNAME
|
| 108 |
|
| 109 |
async def async_step_zeroconf(
|
| 110 |
self, discovery_info: ZeroconfServiceInfo
|
| 111 |
-
) ->
|
| 112 |
"""Prepare configuration for a discovered Plugwise Smile."""
|
| 113 |
self.discovery_info = discovery_info
|
| 114 |
_properties = discovery_info.properties
|
|
@@ -125,7 +143,7 @@
|
|
| 125 |
CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
|
| 126 |
},
|
| 127 |
)
|
| 128 |
-
except Exception: #
|
| 129 |
self._abort_if_unique_id_configured()
|
| 130 |
else:
|
| 131 |
self._abort_if_unique_id_configured(
|
|
@@ -137,167 +155,106 @@
|
|
| 137 |
|
| 138 |
if DEFAULT_USERNAME not in unique_id:
|
| 139 |
self._username = STRETCH_USERNAME
|
| 140 |
-
_product = _properties.get("product",
|
| 141 |
_version = _properties.get("version", "n/a")
|
| 142 |
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
|
| 143 |
|
| 144 |
# This is an Anna, but we already have config entries.
|
| 145 |
# Assuming that the user has already configured Adam, aborting discovery.
|
| 146 |
-
if self._async_current_entries() and _product ==
|
| 147 |
-
return self.async_abort(reason=
|
| 148 |
|
| 149 |
# If we have discovered an Adam or Anna, both might be on the network.
|
| 150 |
# In that case, we need to cancel the Anna flow, as the Adam should
|
| 151 |
# be added.
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
if (
|
| 155 |
-
_product == "smile_thermo"
|
| 156 |
-
and "context" in flow
|
| 157 |
-
and flow["context"].get("product") == "smile_open_therm"
|
| 158 |
-
):
|
| 159 |
-
return self.async_abort(reason="anna_with_adam")
|
| 160 |
-
|
| 161 |
-
# This is an Adam, and there is already an Anna flow in progress
|
| 162 |
-
if (
|
| 163 |
-
_product == "smile_open_therm"
|
| 164 |
-
and "context" in flow
|
| 165 |
-
and flow["context"].get("product") == "smile_thermo"
|
| 166 |
-
and "flow_id" in flow
|
| 167 |
-
):
|
| 168 |
-
self.hass.config_entries.flow.async_abort(flow["flow_id"])
|
| 169 |
|
| 170 |
self.context.update(
|
| 171 |
{
|
| 172 |
-
"title_placeholders": {
|
| 173 |
-
|
| 174 |
-
CONF_NAME: _name,
|
| 175 |
-
CONF_PORT: discovery_info.port,
|
| 176 |
-
CONF_USERNAME: self._username,
|
| 177 |
-
},
|
| 178 |
-
"configuration_url": (
|
| 179 |
f"http://{discovery_info.host}:{discovery_info.port}"
|
| 180 |
),
|
| 181 |
-
"product": _product,
|
| 182 |
}
|
| 183 |
)
|
| 184 |
return await self.async_step_user()
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
async def async_step_user(
|
| 187 |
self, user_input: dict[str, Any] | None = None
|
| 188 |
-
) ->
|
| 189 |
"""Handle the initial step when using network/gateway setups."""
|
| 190 |
errors: dict[str, str] = {}
|
| 191 |
|
| 192 |
-
if not user_input:
|
| 193 |
-
return self.async_show_form(
|
| 194 |
-
step_id="user",
|
| 195 |
-
data_schema=_base_schema(self.discovery_info, None),
|
| 196 |
-
errors=errors,
|
| 197 |
-
)
|
| 198 |
-
|
| 199 |
-
if self.discovery_info:
|
| 200 |
-
user_input[CONF_HOST] = self.discovery_info.host
|
| 201 |
-
user_input[CONF_PORT] = self.discovery_info.port
|
| 202 |
-
user_input[CONF_USERNAME] = self._username
|
| 203 |
-
try:
|
| 204 |
-
api = await validate_input(self.hass, user_input)
|
| 205 |
-
except ConnectionFailedError:
|
| 206 |
-
errors[CONF_BASE] = "cannot_connect"
|
| 207 |
-
except InvalidAuthentication:
|
| 208 |
-
errors[CONF_BASE] = "invalid_auth"
|
| 209 |
-
except InvalidSetupError:
|
| 210 |
-
errors[CONF_BASE] = "invalid_setup"
|
| 211 |
-
except (InvalidXMLError, ResponseError):
|
| 212 |
-
errors[CONF_BASE] = "response_error"
|
| 213 |
-
except UnsupportedDeviceError:
|
| 214 |
-
errors[CONF_BASE] = "unsupported"
|
| 215 |
-
except Exception: # pylint: disable=broad-except
|
| 216 |
-
errors[CONF_BASE] = "unknown"
|
| 217 |
-
|
| 218 |
-
if errors:
|
| 219 |
-
return self.async_show_form(
|
| 220 |
-
step_id="user",
|
| 221 |
-
data_schema=_base_schema(None, user_input),
|
| 222 |
-
errors=errors,
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
await self.async_set_unique_id(
|
| 226 |
-
api.smile_hostname or api.gateway_id, raise_on_progress=False
|
| 227 |
-
)
|
| 228 |
-
self._abort_if_unique_id_configured()
|
| 229 |
-
|
| 230 |
-
return self.async_create_entry(title=api.smile_name, data=user_input)
|
| 231 |
-
|
| 232 |
-
@staticmethod
|
| 233 |
-
@callback
|
| 234 |
-
def async_get_options_flow(
|
| 235 |
-
config_entry: ConfigEntry,
|
| 236 |
-
) -> config_entries.OptionsFlow: # pw-beta options
|
| 237 |
-
"""Get the options flow for this handler."""
|
| 238 |
-
return PlugwiseOptionsFlowHandler(config_entry)
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
# pw-beta - change the scan-interval via CONFIGURE
|
| 242 |
-
# pw-beta - add homekit emulation via CONFIGURE
|
| 243 |
-
# pw-beta - change the frontend refresh interval via CONFIGURE
|
| 244 |
-
class PlugwiseOptionsFlowHandler(config_entries.OptionsFlow): # pw-beta options
|
| 245 |
-
"""Plugwise option flow."""
|
| 246 |
-
|
| 247 |
-
def __init__(self, config_entry: ConfigEntry) -> None: # pragma: no cover
|
| 248 |
-
"""Initialize options flow."""
|
| 249 |
-
self.config_entry = config_entry
|
| 250 |
-
|
| 251 |
-
async def async_step_none(
|
| 252 |
-
self, user_input: dict[str, Any] | None = None
|
| 253 |
-
) -> FlowResult: # pragma: no cover
|
| 254 |
-
"""No options available."""
|
| 255 |
if user_input is not None:
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
-
return self.async_show_form(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
-
async def
|
| 262 |
self, user_input: dict[str, Any] | None = None
|
| 263 |
-
) ->
|
| 264 |
-
"""
|
| 265 |
-
|
| 266 |
-
return await self.async_step_none(user_input)
|
| 267 |
-
|
| 268 |
-
if user_input is not None:
|
| 269 |
-
return self.async_create_entry(title="", data=user_input)
|
| 270 |
-
|
| 271 |
-
coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id][COORDINATOR]
|
| 272 |
-
interval: dt.timedelta = DEFAULT_SCAN_INTERVAL[
|
| 273 |
-
coordinator.api.smile_type
|
| 274 |
-
] # pw-beta options
|
| 275 |
-
|
| 276 |
-
data = {
|
| 277 |
-
vol.Optional(
|
| 278 |
-
CONF_SCAN_INTERVAL,
|
| 279 |
-
default=self.config_entry.options.get(
|
| 280 |
-
CONF_SCAN_INTERVAL, interval.seconds
|
| 281 |
-
),
|
| 282 |
-
): vol.All(cv.positive_int, vol.Clamp(min=10)),
|
| 283 |
-
} # pw-beta
|
| 284 |
|
| 285 |
-
|
| 286 |
-
return self.async_show_form(step_id="init", data_schema=vol.Schema(data))
|
| 287 |
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
): cv.boolean,
|
| 296 |
-
vol.Optional(
|
| 297 |
-
CONF_REFRESH_INTERVAL,
|
| 298 |
-
default=self.config_entry.options.get(CONF_REFRESH_INTERVAL, 1.5),
|
| 299 |
-
): vol.All(vol.Coerce(float), vol.Range(min=1.5, max=10.0)),
|
| 300 |
}
|
| 301 |
-
) # pw-beta
|
| 302 |
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Config flow for Plugwise integration."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Any, Self
|
| 7 |
|
| 8 |
from plugwise import Smile
|
| 9 |
from plugwise.exceptions import (
|
|
|
|
| 16 |
)
|
| 17 |
import voluptuous as vol
|
| 18 |
|
| 19 |
+
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
|
|
|
|
|
|
|
| 20 |
from homeassistant.const import (
|
| 21 |
+
ATTR_CONFIGURATION_URL,
|
| 22 |
CONF_BASE,
|
| 23 |
CONF_HOST,
|
| 24 |
CONF_NAME,
|
| 25 |
CONF_PASSWORD,
|
| 26 |
CONF_PORT,
|
|
|
|
| 27 |
CONF_USERNAME,
|
| 28 |
)
|
| 29 |
+
from homeassistant.core import HomeAssistant
|
|
|
|
|
|
|
| 30 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
| 31 |
+
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
| 32 |
|
| 33 |
from .const import (
|
| 34 |
+
ANNA_WITH_ADAM,
|
|
|
|
|
|
|
| 35 |
DEFAULT_PORT,
|
|
|
|
| 36 |
DEFAULT_USERNAME,
|
| 37 |
DOMAIN,
|
| 38 |
FLOW_SMILE,
|
| 39 |
FLOW_STRETCH,
|
| 40 |
SMILE,
|
| 41 |
+
SMILE_OPEN_THERM,
|
| 42 |
+
SMILE_THERMO,
|
| 43 |
STRETCH,
|
| 44 |
STRETCH_USERNAME,
|
| 45 |
+
UNKNOWN_SMILE,
|
| 46 |
ZEROCONF_MAP,
|
| 47 |
)
|
| 48 |
|
| 49 |
+
_LOGGER = logging.getLogger(__name__)
|
| 50 |
+
|
| 51 |
+
SMILE_RECONF_SCHEMA = vol.Schema(
|
| 52 |
+
{
|
| 53 |
+
vol.Required(CONF_HOST): str,
|
| 54 |
+
}
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
|
| 58 |
+
def smile_user_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
|
|
|
|
|
|
|
|
|
|
| 59 |
"""Generate base schema for gateways."""
|
| 60 |
+
schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
|
| 61 |
+
|
| 62 |
if not discovery_info:
|
| 63 |
+
schema = schema.extend(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
{
|
| 65 |
+
vol.Required(CONF_HOST): str,
|
| 66 |
+
vol.Required(CONF_USERNAME, default=SMILE): vol.In(
|
|
|
|
|
|
|
| 67 |
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
| 68 |
),
|
| 69 |
}
|
| 70 |
)
|
| 71 |
|
| 72 |
+
return schema
|
| 73 |
|
| 74 |
|
| 75 |
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
|
| 76 |
"""Validate whether the user input allows us to connect to the gateway.
|
| 77 |
|
| 78 |
+
Data has the keys from the schema with values provided by the user.
|
| 79 |
"""
|
| 80 |
websession = async_get_clientsession(hass, verify_ssl=False)
|
| 81 |
api = Smile(
|
|
|
|
| 83 |
password=data[CONF_PASSWORD],
|
| 84 |
port=data[CONF_PORT],
|
| 85 |
username=data[CONF_USERNAME],
|
|
|
|
| 86 |
websession=websession,
|
| 87 |
)
|
| 88 |
await api.connect()
|
| 89 |
return api
|
| 90 |
|
| 91 |
|
| 92 |
+
async def verify_connection(
|
| 93 |
+
hass: HomeAssistant, user_input: dict[str, Any]
|
| 94 |
+
) -> tuple[Smile | None, dict[str, str]]:
|
| 95 |
+
"""Verify and return the gateway connection or an error."""
|
| 96 |
+
errors: dict[str, str] = {}
|
| 97 |
+
|
| 98 |
+
try:
|
| 99 |
+
return (await validate_input(hass, user_input), errors)
|
| 100 |
+
except ConnectionFailedError:
|
| 101 |
+
errors[CONF_BASE] = "cannot_connect"
|
| 102 |
+
except InvalidAuthentication:
|
| 103 |
+
errors[CONF_BASE] = "invalid_auth"
|
| 104 |
+
except InvalidSetupError:
|
| 105 |
+
errors[CONF_BASE] = "invalid_setup"
|
| 106 |
+
except (InvalidXMLError, ResponseError):
|
| 107 |
+
errors[CONF_BASE] = "response_error"
|
| 108 |
+
except UnsupportedDeviceError:
|
| 109 |
+
errors[CONF_BASE] = "unsupported"
|
| 110 |
+
except Exception:
|
| 111 |
+
_LOGGER.exception(
|
| 112 |
+
"Unknown exception while verifying connection with your Plugwise Smile"
|
| 113 |
+
)
|
| 114 |
+
errors[CONF_BASE] = "unknown"
|
| 115 |
+
return (None, errors)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
| 119 |
"""Handle a config flow for Plugwise Smile."""
|
| 120 |
|
| 121 |
VERSION = 1
|
| 122 |
|
| 123 |
discovery_info: ZeroconfServiceInfo | None = None
|
| 124 |
+
product: str = UNKNOWN_SMILE
|
| 125 |
_username: str = DEFAULT_USERNAME
|
| 126 |
|
| 127 |
async def async_step_zeroconf(
|
| 128 |
self, discovery_info: ZeroconfServiceInfo
|
| 129 |
+
) -> ConfigFlowResult:
|
| 130 |
"""Prepare configuration for a discovered Plugwise Smile."""
|
| 131 |
self.discovery_info = discovery_info
|
| 132 |
_properties = discovery_info.properties
|
|
|
|
| 143 |
CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
|
| 144 |
},
|
| 145 |
)
|
| 146 |
+
except Exception: # noqa: BLE001
|
| 147 |
self._abort_if_unique_id_configured()
|
| 148 |
else:
|
| 149 |
self._abort_if_unique_id_configured(
|
|
|
|
| 155 |
|
| 156 |
if DEFAULT_USERNAME not in unique_id:
|
| 157 |
self._username = STRETCH_USERNAME
|
| 158 |
+
self.product = _product = _properties.get("product", UNKNOWN_SMILE)
|
| 159 |
_version = _properties.get("version", "n/a")
|
| 160 |
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
|
| 161 |
|
| 162 |
# This is an Anna, but we already have config entries.
|
| 163 |
# Assuming that the user has already configured Adam, aborting discovery.
|
| 164 |
+
if self._async_current_entries() and _product == SMILE_THERMO:
|
| 165 |
+
return self.async_abort(reason=ANNA_WITH_ADAM)
|
| 166 |
|
| 167 |
# If we have discovered an Adam or Anna, both might be on the network.
|
| 168 |
# In that case, we need to cancel the Anna flow, as the Adam should
|
| 169 |
# be added.
|
| 170 |
+
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
| 171 |
+
return self.async_abort(reason=ANNA_WITH_ADAM)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
self.context.update(
|
| 174 |
{
|
| 175 |
+
"title_placeholders": {CONF_NAME: _name},
|
| 176 |
+
ATTR_CONFIGURATION_URL: (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
f"http://{discovery_info.host}:{discovery_info.port}"
|
| 178 |
),
|
|
|
|
| 179 |
}
|
| 180 |
)
|
| 181 |
return await self.async_step_user()
|
| 182 |
|
| 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
|
| 186 |
+
if self.product == SMILE_THERMO and other_flow.product == SMILE_OPEN_THERM:
|
| 187 |
+
return True
|
| 188 |
+
|
| 189 |
+
# This is an Adam, and there is already an Anna flow in progress
|
| 190 |
+
if self.product == SMILE_OPEN_THERM and other_flow.product == SMILE_THERMO:
|
| 191 |
+
self.hass.config_entries.flow.async_abort(other_flow.flow_id)
|
| 192 |
+
|
| 193 |
+
return False
|
| 194 |
+
|
| 195 |
async def async_step_user(
|
| 196 |
self, user_input: dict[str, Any] | None = None
|
| 197 |
+
) -> ConfigFlowResult:
|
| 198 |
"""Handle the initial step when using network/gateway setups."""
|
| 199 |
errors: dict[str, str] = {}
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
if user_input is not None:
|
| 202 |
+
user_input[CONF_PORT] = DEFAULT_PORT
|
| 203 |
+
if self.discovery_info:
|
| 204 |
+
user_input[CONF_HOST] = self.discovery_info.host
|
| 205 |
+
user_input[CONF_PORT] = self.discovery_info.port
|
| 206 |
+
user_input[CONF_USERNAME] = self._username
|
| 207 |
+
|
| 208 |
+
api, errors = await verify_connection(self.hass, user_input)
|
| 209 |
+
if api:
|
| 210 |
+
await self.async_set_unique_id(
|
| 211 |
+
api.smile.hostname or api.gateway_id,
|
| 212 |
+
raise_on_progress=False,
|
| 213 |
+
)
|
| 214 |
+
self._abort_if_unique_id_configured()
|
| 215 |
+
return self.async_create_entry(title=api.smile.name, data=user_input)
|
| 216 |
|
| 217 |
+
return self.async_show_form(
|
| 218 |
+
step_id=SOURCE_USER,
|
| 219 |
+
data_schema=smile_user_schema(self.discovery_info),
|
| 220 |
+
errors=errors,
|
| 221 |
+
)
|
| 222 |
|
| 223 |
+
async def async_step_reconfigure(
|
| 224 |
self, user_input: dict[str, Any] | None = None
|
| 225 |
+
) -> ConfigFlowResult:
|
| 226 |
+
"""Handle reconfiguration of the integration."""
|
| 227 |
+
errors: dict[str, str] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
+
reconfigure_entry = self._get_reconfigure_entry()
|
|
|
|
| 230 |
|
| 231 |
+
if user_input:
|
| 232 |
+
# Keep current username and password
|
| 233 |
+
full_input = {
|
| 234 |
+
CONF_HOST: user_input.get(CONF_HOST),
|
| 235 |
+
CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
|
| 236 |
+
CONF_USERNAME: reconfigure_entry.data.get(CONF_USERNAME),
|
| 237 |
+
CONF_PASSWORD: reconfigure_entry.data.get(CONF_PASSWORD),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
}
|
|
|
|
| 239 |
|
| 240 |
+
api, errors = await verify_connection(self.hass, full_input)
|
| 241 |
+
if api:
|
| 242 |
+
await self.async_set_unique_id(
|
| 243 |
+
api.smile.hostname or api.gateway_id,
|
| 244 |
+
raise_on_progress=False,
|
| 245 |
+
)
|
| 246 |
+
self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
|
| 247 |
+
return self.async_update_reload_and_abort(
|
| 248 |
+
reconfigure_entry,
|
| 249 |
+
data_updates=full_input,
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
return self.async_show_form(
|
| 253 |
+
step_id="reconfigure",
|
| 254 |
+
data_schema=self.add_suggested_values_to_schema(
|
| 255 |
+
data_schema=SMILE_RECONF_SCHEMA,
|
| 256 |
+
suggested_values=reconfigure_entry.data,
|
| 257 |
+
),
|
| 258 |
+
description_placeholders={"title": reconfigure_entry.title},
|
| 259 |
+
errors=errors,
|
| 260 |
+
)
|
|
@@ -1,7 +1,10 @@
|
|
| 1 |
"""Constants for Plugwise component."""
|
|
|
|
|
|
|
|
|
|
| 2 |
from datetime import timedelta
|
| 3 |
import logging
|
| 4 |
-
from typing import Final
|
| 5 |
|
| 6 |
from homeassistant.const import Platform
|
| 7 |
|
|
@@ -9,56 +12,82 @@
|
|
| 9 |
|
| 10 |
LOGGER = logging.getLogger(__package__)
|
| 11 |
|
|
|
|
| 12 |
API: Final = "api"
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
SMILE: Final = "smile"
|
|
|
|
|
|
|
| 18 |
STRETCH: Final = "stretch"
|
| 19 |
STRETCH_USERNAME: Final = "stretch"
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# Default directives
|
|
|
|
|
|
|
| 29 |
DEFAULT_PORT: Final = 80
|
| 30 |
DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
|
| 31 |
"power": timedelta(seconds=10),
|
| 32 |
"stretch": timedelta(seconds=60),
|
| 33 |
"thermostat": timedelta(seconds=60),
|
| 34 |
}
|
| 35 |
-
DEFAULT_TIMEOUT: Final = 10
|
| 36 |
DEFAULT_USERNAME: Final = "smile"
|
| 37 |
|
| 38 |
-
# --- Const for Plugwise Smile and Stretch
|
| 39 |
-
PLATFORMS: Final[list[str]] = [
|
| 40 |
-
Platform.BINARY_SENSOR,
|
| 41 |
-
Platform.CLIMATE,
|
| 42 |
-
Platform.NUMBER,
|
| 43 |
-
Platform.SELECT,
|
| 44 |
-
Platform.SENSOR,
|
| 45 |
-
Platform.SWITCH,
|
| 46 |
-
]
|
| 47 |
-
SERVICE_DELETE: Final = "delete_notification"
|
| 48 |
-
SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
|
| 49 |
-
|
| 50 |
-
# Climate const:
|
| 51 |
MASTER_THERMOSTATS: Final[list[str]] = [
|
| 52 |
"thermostat",
|
|
|
|
| 53 |
"zone_thermometer",
|
| 54 |
"zone_thermostat",
|
| 55 |
-
"thermostatic_radiator_valve",
|
| 56 |
]
|
| 57 |
|
| 58 |
-
#
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
}
|
|
|
|
| 1 |
"""Constants for Plugwise component."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
from datetime import timedelta
|
| 6 |
import logging
|
| 7 |
+
from typing import Final, Literal
|
| 8 |
|
| 9 |
from homeassistant.const import Platform
|
| 10 |
|
|
|
|
| 12 |
|
| 13 |
LOGGER = logging.getLogger(__package__)
|
| 14 |
|
| 15 |
+
ANNA_WITH_ADAM: Final = "anna_with_adam"
|
| 16 |
API: Final = "api"
|
| 17 |
+
AVAILABLE: Final = "available"
|
| 18 |
+
DEV_CLASS: Final = "dev_class"
|
| 19 |
+
FLOW_SMILE: Final = "smile (Adam/Anna/P1)"
|
| 20 |
+
FLOW_STRETCH: Final = "stretch (Stretch)"
|
| 21 |
+
FLOW_TYPE: Final = "flow_type"
|
| 22 |
+
GATEWAY: Final = "gateway"
|
| 23 |
+
LOCATION: Final = "location"
|
| 24 |
+
PW_TYPE: Final = "plugwise_type"
|
| 25 |
+
REBOOT: Final = "reboot"
|
| 26 |
SMILE: Final = "smile"
|
| 27 |
+
SMILE_OPEN_THERM: Final = "smile_open_therm"
|
| 28 |
+
SMILE_THERMO: Final = "smile_thermo"
|
| 29 |
STRETCH: Final = "stretch"
|
| 30 |
STRETCH_USERNAME: Final = "stretch"
|
| 31 |
+
UNKNOWN_SMILE: Final = "Unknown Smile"
|
| 32 |
|
| 33 |
+
PLATFORMS: Final[list[str]] = [
|
| 34 |
+
Platform.BINARY_SENSOR,
|
| 35 |
+
Platform.BUTTON,
|
| 36 |
+
Platform.CLIMATE,
|
| 37 |
+
Platform.NUMBER,
|
| 38 |
+
Platform.SELECT,
|
| 39 |
+
Platform.SENSOR,
|
| 40 |
+
Platform.SWITCH,
|
| 41 |
+
]
|
| 42 |
+
ZEROCONF_MAP: Final[dict[str, str]] = {
|
| 43 |
+
"smile": "Smile P1",
|
| 44 |
+
"smile_thermo": "Smile Anna",
|
| 45 |
+
"smile_open_therm": "Adam",
|
| 46 |
+
"stretch": "Stretch",
|
| 47 |
+
}
|
| 48 |
|
| 49 |
+
type NumberType = Literal[
|
| 50 |
+
"maximum_boiler_temperature",
|
| 51 |
+
"max_dhw_temperature",
|
| 52 |
+
"temperature_offset",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
type SelectType = Literal[
|
| 56 |
+
"select_dhw_mode",
|
| 57 |
+
"select_gateway_mode",
|
| 58 |
+
"select_regulation_mode",
|
| 59 |
+
"select_schedule",
|
| 60 |
+
"select_zone_profile",
|
| 61 |
+
]
|
| 62 |
+
type SelectOptionsType = Literal[
|
| 63 |
+
"available_schedules",
|
| 64 |
+
"dhw_modes",
|
| 65 |
+
"gateway_modes",
|
| 66 |
+
"regulation_modes",
|
| 67 |
+
"zone_profiles",
|
| 68 |
+
]
|
| 69 |
|
| 70 |
# Default directives
|
| 71 |
+
DEFAULT_MAX_TEMP: Final = 30
|
| 72 |
+
DEFAULT_MIN_TEMP: Final = 4
|
| 73 |
DEFAULT_PORT: Final = 80
|
| 74 |
DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
|
| 75 |
"power": timedelta(seconds=10),
|
| 76 |
"stretch": timedelta(seconds=60),
|
| 77 |
"thermostat": timedelta(seconds=60),
|
| 78 |
}
|
|
|
|
| 79 |
DEFAULT_USERNAME: Final = "smile"
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
MASTER_THERMOSTATS: Final[list[str]] = [
|
| 82 |
"thermostat",
|
| 83 |
+
"thermostatic_radiator_valve",
|
| 84 |
"zone_thermometer",
|
| 85 |
"zone_thermostat",
|
|
|
|
| 86 |
]
|
| 87 |
|
| 88 |
+
# Select constants
|
| 89 |
+
SELECT_DHW_MODE: Final = "select_dhw_mode"
|
| 90 |
+
SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
|
| 91 |
+
SELECT_REGULATION_MODE: Final = "select_regulation_mode"
|
| 92 |
+
SELECT_SCHEDULE: Final = "select_schedule"
|
| 93 |
+
SELECT_ZONE_PROFILE: Final = "select_zone_profile"
|
|
|
|
@@ -1,116 +1,143 @@
|
|
| 1 |
"""DataUpdateCoordinator for Plugwise."""
|
|
|
|
| 2 |
from datetime import timedelta
|
| 3 |
|
| 4 |
-
from
|
|
|
|
| 5 |
from plugwise.exceptions import (
|
| 6 |
ConnectionFailedError,
|
| 7 |
InvalidAuthentication,
|
| 8 |
InvalidXMLError,
|
|
|
|
| 9 |
ResponseError,
|
| 10 |
UnsupportedDeviceError,
|
| 11 |
)
|
| 12 |
|
| 13 |
from homeassistant.config_entries import ConfigEntry
|
| 14 |
-
from homeassistant.const import
|
| 15 |
-
CONF_HOST,
|
| 16 |
-
CONF_PASSWORD,
|
| 17 |
-
CONF_PORT,
|
| 18 |
-
CONF_SCAN_INTERVAL, # pw-beta options
|
| 19 |
-
CONF_USERNAME,
|
| 20 |
-
)
|
| 21 |
from homeassistant.core import HomeAssistant
|
| 22 |
from homeassistant.exceptions import ConfigEntryError
|
|
|
|
| 23 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
| 24 |
from homeassistant.helpers.debounce import Debouncer
|
| 25 |
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
| 26 |
|
| 27 |
-
from .const import DEFAULT_PORT,
|
|
|
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
-
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[
|
| 31 |
"""Class to manage fetching Plugwise data from single endpoint."""
|
| 32 |
|
| 33 |
_connected: bool = False
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
entry: ConfigEntry,
|
| 39 |
-
cooldown: float,
|
| 40 |
-
update_interval: timedelta = timedelta(seconds=60),
|
| 41 |
-
) -> None: # pw-beta cooldown
|
| 42 |
"""Initialize the coordinator."""
|
| 43 |
super().__init__(
|
| 44 |
hass,
|
| 45 |
LOGGER,
|
|
|
|
| 46 |
name=DOMAIN,
|
| 47 |
-
|
| 48 |
-
update_interval=update_interval,
|
| 49 |
# Don't refresh immediately, give the device time to process
|
| 50 |
# the change in state before we query it.
|
| 51 |
request_refresh_debouncer=Debouncer(
|
| 52 |
hass,
|
| 53 |
LOGGER,
|
| 54 |
-
cooldown=
|
| 55 |
immediate=False,
|
| 56 |
),
|
| 57 |
)
|
| 58 |
|
| 59 |
self.api = Smile(
|
| 60 |
-
host=
|
| 61 |
-
username=
|
| 62 |
-
password=
|
| 63 |
-
port=
|
| 64 |
-
timeout=30,
|
| 65 |
websession=async_get_clientsession(hass, verify_ssl=False),
|
| 66 |
)
|
| 67 |
-
self.
|
| 68 |
-
self.
|
| 69 |
-
self.update_interval = update_interval
|
| 70 |
|
| 71 |
async def _connect(self) -> None:
|
| 72 |
"""Connect to the Plugwise Smile."""
|
| 73 |
-
|
| 74 |
-
self.
|
| 75 |
-
|
| 76 |
-
self.update_interval = DEFAULT_SCAN_INTERVAL.get(
|
| 77 |
-
self.api.smile_type, timedelta(seconds=60)
|
| 78 |
-
) # pw-beta options scan-interval
|
| 79 |
-
if (custom_time := self._entry.options.get(CONF_SCAN_INTERVAL)) is not None:
|
| 80 |
-
self.update_interval = timedelta(
|
| 81 |
-
seconds=int(custom_time)
|
| 82 |
-
) # pragma: no cover # pw-beta options
|
| 83 |
-
|
| 84 |
-
LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
|
| 85 |
|
| 86 |
-
async def _async_update_data(self) ->
|
| 87 |
"""Fetch data from Plugwise."""
|
| 88 |
-
data = PlugwiseData(gateway={}, devices={})
|
| 89 |
-
|
| 90 |
try:
|
| 91 |
if not self._connected:
|
| 92 |
await self._connect()
|
| 93 |
data = await self.api.async_update()
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
except InvalidAuthentication as err:
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
| 101 |
except (InvalidXMLError, ResponseError) as err:
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
except UnsupportedDeviceError as err:
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
if not self._unavailable_logged: # pw-beta add to Core
|
| 113 |
-
self._unavailable_logged = True
|
| 114 |
-
raise UpdateFailed("Failed to connect") from err
|
| 115 |
|
|
|
|
| 116 |
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""DataUpdateCoordinator for Plugwise."""
|
| 2 |
+
|
| 3 |
from datetime import timedelta
|
| 4 |
|
| 5 |
+
from packaging.version import Version
|
| 6 |
+
from plugwise import GwEntityData, Smile
|
| 7 |
from plugwise.exceptions import (
|
| 8 |
ConnectionFailedError,
|
| 9 |
InvalidAuthentication,
|
| 10 |
InvalidXMLError,
|
| 11 |
+
PlugwiseError,
|
| 12 |
ResponseError,
|
| 13 |
UnsupportedDeviceError,
|
| 14 |
)
|
| 15 |
|
| 16 |
from homeassistant.config_entries import ConfigEntry
|
| 17 |
+
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
from homeassistant.core import HomeAssistant
|
| 19 |
from homeassistant.exceptions import ConfigEntryError
|
| 20 |
+
from homeassistant.helpers import device_registry as dr
|
| 21 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
| 22 |
from homeassistant.helpers.debounce import Debouncer
|
| 23 |
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
| 24 |
|
| 25 |
+
from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, LOGGER
|
| 26 |
+
|
| 27 |
+
type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
|
| 28 |
|
| 29 |
|
| 30 |
+
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData]]):
|
| 31 |
"""Class to manage fetching Plugwise data from single endpoint."""
|
| 32 |
|
| 33 |
_connected: bool = False
|
| 34 |
|
| 35 |
+
config_entry: PlugwiseConfigEntry
|
| 36 |
+
|
| 37 |
+
def __init__(self, hass: HomeAssistant, config_entry: PlugwiseConfigEntry) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
"""Initialize the coordinator."""
|
| 39 |
super().__init__(
|
| 40 |
hass,
|
| 41 |
LOGGER,
|
| 42 |
+
config_entry=config_entry,
|
| 43 |
name=DOMAIN,
|
| 44 |
+
update_interval=timedelta(seconds=60),
|
|
|
|
| 45 |
# Don't refresh immediately, give the device time to process
|
| 46 |
# the change in state before we query it.
|
| 47 |
request_refresh_debouncer=Debouncer(
|
| 48 |
hass,
|
| 49 |
LOGGER,
|
| 50 |
+
cooldown=1.5,
|
| 51 |
immediate=False,
|
| 52 |
),
|
| 53 |
)
|
| 54 |
|
| 55 |
self.api = Smile(
|
| 56 |
+
host=self.config_entry.data[CONF_HOST],
|
| 57 |
+
username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
|
| 58 |
+
password=self.config_entry.data[CONF_PASSWORD],
|
| 59 |
+
port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
|
|
|
|
| 60 |
websession=async_get_clientsession(hass, verify_ssl=False),
|
| 61 |
)
|
| 62 |
+
self._current_devices: set[str] = set()
|
| 63 |
+
self.new_devices: set[str] = set()
|
|
|
|
| 64 |
|
| 65 |
async def _connect(self) -> None:
|
| 66 |
"""Connect to the Plugwise Smile."""
|
| 67 |
+
version = await self.api.connect()
|
| 68 |
+
self._connected = isinstance(version, Version)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
async def _async_update_data(self) -> dict[str, GwEntityData]:
|
| 71 |
"""Fetch data from Plugwise."""
|
|
|
|
|
|
|
| 72 |
try:
|
| 73 |
if not self._connected:
|
| 74 |
await self._connect()
|
| 75 |
data = await self.api.async_update()
|
| 76 |
+
except ConnectionFailedError as err:
|
| 77 |
+
raise UpdateFailed(
|
| 78 |
+
translation_domain=DOMAIN,
|
| 79 |
+
translation_key="failed_to_connect",
|
| 80 |
+
) from err
|
| 81 |
except InvalidAuthentication as err:
|
| 82 |
+
raise ConfigEntryError(
|
| 83 |
+
translation_domain=DOMAIN,
|
| 84 |
+
translation_key="authentication_failed",
|
| 85 |
+
) from err
|
| 86 |
except (InvalidXMLError, ResponseError) as err:
|
| 87 |
+
raise UpdateFailed(
|
| 88 |
+
translation_domain=DOMAIN,
|
| 89 |
+
translation_key="invalid_xml_data",
|
| 90 |
+
) from err
|
| 91 |
+
except PlugwiseError as err:
|
| 92 |
+
raise UpdateFailed(
|
| 93 |
+
translation_domain=DOMAIN,
|
| 94 |
+
translation_key="data_incomplete_or_missing",
|
| 95 |
+
) from err
|
| 96 |
except UnsupportedDeviceError as err:
|
| 97 |
+
raise ConfigEntryError(
|
| 98 |
+
translation_domain=DOMAIN,
|
| 99 |
+
translation_key="unsupported_firmware",
|
| 100 |
+
) from err
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
self._async_add_remove_devices(data)
|
| 103 |
return data
|
| 104 |
+
|
| 105 |
+
def _async_add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
|
| 106 |
+
"""Add new Plugwise devices, remove non-existing devices."""
|
| 107 |
+
# Check for new or removed devices
|
| 108 |
+
self.new_devices = set(data) - self._current_devices
|
| 109 |
+
removed_devices = self._current_devices - set(data)
|
| 110 |
+
self._current_devices = set(data)
|
| 111 |
+
|
| 112 |
+
if removed_devices:
|
| 113 |
+
self._async_remove_devices(data)
|
| 114 |
+
|
| 115 |
+
def _async_remove_devices(self, data: dict[str, GwEntityData]) -> None:
|
| 116 |
+
"""Clean registries when removed devices found."""
|
| 117 |
+
device_reg = dr.async_get(self.hass)
|
| 118 |
+
device_list = dr.async_entries_for_config_entry(
|
| 119 |
+
device_reg, self.config_entry.entry_id
|
| 120 |
+
)
|
| 121 |
+
# First find the Plugwise via_device
|
| 122 |
+
gateway_device = device_reg.async_get_device({(DOMAIN, self.api.gateway_id)})
|
| 123 |
+
assert gateway_device is not None
|
| 124 |
+
via_device_id = gateway_device.id
|
| 125 |
+
|
| 126 |
+
# Then remove the connected orphaned device(s)
|
| 127 |
+
for device_entry in device_list:
|
| 128 |
+
for identifier in device_entry.identifiers:
|
| 129 |
+
if identifier[0] == DOMAIN:
|
| 130 |
+
if (
|
| 131 |
+
device_entry.via_device_id == via_device_id
|
| 132 |
+
and identifier[1] not in data
|
| 133 |
+
):
|
| 134 |
+
device_reg.async_update_device(
|
| 135 |
+
device_entry.id,
|
| 136 |
+
remove_config_entry_id=self.config_entry.entry_id,
|
| 137 |
+
)
|
| 138 |
+
LOGGER.debug(
|
| 139 |
+
"Removed %s device %s %s from device_registry",
|
| 140 |
+
DOMAIN,
|
| 141 |
+
device_entry.model,
|
| 142 |
+
identifier[1],
|
| 143 |
+
)
|
|
@@ -1,26 +1,17 @@
|
|
| 1 |
"""Diagnostics support for Plugwise."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
-
from homeassistant.config_entries import ConfigEntry
|
| 7 |
from homeassistant.core import HomeAssistant
|
| 8 |
|
| 9 |
-
from .
|
| 10 |
-
COORDINATOR, # pw-beta
|
| 11 |
-
DOMAIN,
|
| 12 |
-
)
|
| 13 |
-
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 14 |
|
| 15 |
|
| 16 |
async def async_get_config_entry_diagnostics(
|
| 17 |
-
hass: HomeAssistant, entry:
|
| 18 |
) -> dict[str, Any]:
|
| 19 |
"""Return diagnostics for a config entry."""
|
| 20 |
-
coordinator
|
| 21 |
-
|
| 22 |
-
]
|
| 23 |
-
return {
|
| 24 |
-
"gateway": coordinator.data.gateway,
|
| 25 |
-
"devices": coordinator.data.devices,
|
| 26 |
-
}
|
|
|
|
| 1 |
"""Diagnostics support for Plugwise."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from typing import Any
|
| 6 |
|
|
|
|
| 7 |
from homeassistant.core import HomeAssistant
|
| 8 |
|
| 9 |
+
from .coordinator import PlugwiseConfigEntry
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
async def async_get_config_entry_diagnostics(
|
| 13 |
+
hass: HomeAssistant, entry: PlugwiseConfigEntry
|
| 14 |
) -> dict[str, Any]:
|
| 15 |
"""Return diagnostics for a config entry."""
|
| 16 |
+
coordinator = entry.runtime_data
|
| 17 |
+
return coordinator.data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,17 +1,18 @@
|
|
| 1 |
"""Generic Plugwise Entity Class."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
-
from plugwise.constants import
|
| 5 |
|
| 6 |
from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
|
| 7 |
from homeassistant.helpers.device_registry import (
|
| 8 |
CONNECTION_NETWORK_MAC,
|
| 9 |
CONNECTION_ZIGBEE,
|
|
|
|
| 10 |
)
|
| 11 |
-
from homeassistant.helpers.entity import DeviceInfo
|
| 12 |
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
| 13 |
|
| 14 |
-
from .const import DOMAIN
|
| 15 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 16 |
|
| 17 |
|
|
@@ -33,7 +34,7 @@
|
|
| 33 |
if entry := self.coordinator.config_entry:
|
| 34 |
configuration_url = f"http://{entry.data[CONF_HOST]}"
|
| 35 |
|
| 36 |
-
data = coordinator.data
|
| 37 |
connections = set()
|
| 38 |
if mac := data.get("mac_address"):
|
| 39 |
connections.add((CONNECTION_NETWORK_MAC, mac))
|
|
@@ -46,18 +47,19 @@
|
|
| 46 |
connections=connections,
|
| 47 |
manufacturer=data.get("vendor"),
|
| 48 |
model=data.get("model"),
|
| 49 |
-
|
|
|
|
| 50 |
sw_version=data.get("firmware"),
|
| 51 |
hw_version=data.get("hardware"),
|
| 52 |
)
|
| 53 |
|
| 54 |
-
if device_id != coordinator.
|
| 55 |
self._attr_device_info.update(
|
| 56 |
{
|
| 57 |
-
ATTR_NAME: data.get(
|
| 58 |
ATTR_VIA_DEVICE: (
|
| 59 |
DOMAIN,
|
| 60 |
-
str(self.coordinator.
|
| 61 |
),
|
| 62 |
}
|
| 63 |
)
|
|
@@ -66,19 +68,12 @@
|
|
| 66 |
def available(self) -> bool:
|
| 67 |
"""Return if entity is available."""
|
| 68 |
return (
|
| 69 |
-
self._dev_id in self.coordinator.data
|
| 70 |
-
|
| 71 |
-
# do not provide their availability-status!
|
| 72 |
-
and ("available" not in self.device or self.device["available"] is True)
|
| 73 |
and super().available
|
| 74 |
)
|
| 75 |
|
| 76 |
@property
|
| 77 |
-
def device(self) ->
|
| 78 |
"""Return data for this device."""
|
| 79 |
-
return self.coordinator.data
|
| 80 |
-
|
| 81 |
-
async def async_added_to_hass(self) -> None:
|
| 82 |
-
"""Subscribe to updates."""
|
| 83 |
-
self._handle_coordinator_update()
|
| 84 |
-
await super().async_added_to_hass()
|
|
|
|
| 1 |
"""Generic Plugwise Entity Class."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from plugwise.constants import GwEntityData
|
| 6 |
|
| 7 |
from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
|
| 8 |
from homeassistant.helpers.device_registry import (
|
| 9 |
CONNECTION_NETWORK_MAC,
|
| 10 |
CONNECTION_ZIGBEE,
|
| 11 |
+
DeviceInfo,
|
| 12 |
)
|
|
|
|
| 13 |
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
| 14 |
|
| 15 |
+
from .const import AVAILABLE, DOMAIN
|
| 16 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 17 |
|
| 18 |
|
|
|
|
| 34 |
if entry := self.coordinator.config_entry:
|
| 35 |
configuration_url = f"http://{entry.data[CONF_HOST]}"
|
| 36 |
|
| 37 |
+
data = coordinator.data[device_id]
|
| 38 |
connections = set()
|
| 39 |
if mac := data.get("mac_address"):
|
| 40 |
connections.add((CONNECTION_NETWORK_MAC, mac))
|
|
|
|
| 47 |
connections=connections,
|
| 48 |
manufacturer=data.get("vendor"),
|
| 49 |
model=data.get("model"),
|
| 50 |
+
model_id=data.get("model_id"),
|
| 51 |
+
name=coordinator.api.smile.name,
|
| 52 |
sw_version=data.get("firmware"),
|
| 53 |
hw_version=data.get("hardware"),
|
| 54 |
)
|
| 55 |
|
| 56 |
+
if device_id != coordinator.api.gateway_id:
|
| 57 |
self._attr_device_info.update(
|
| 58 |
{
|
| 59 |
+
ATTR_NAME: data.get(ATTR_NAME),
|
| 60 |
ATTR_VIA_DEVICE: (
|
| 61 |
DOMAIN,
|
| 62 |
+
str(self.coordinator.api.gateway_id),
|
| 63 |
),
|
| 64 |
}
|
| 65 |
)
|
|
|
|
| 68 |
def available(self) -> bool:
|
| 69 |
"""Return if entity is available."""
|
| 70 |
return (
|
| 71 |
+
self._dev_id in self.coordinator.data
|
| 72 |
+
and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
|
|
|
|
|
|
|
| 73 |
and super().available
|
| 74 |
)
|
| 75 |
|
| 76 |
@property
|
| 77 |
+
def device(self) -> GwEntityData:
|
| 78 |
"""Return data for this device."""
|
| 79 |
+
return self.coordinator.data[self._dev_id]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,13 +1,13 @@
|
|
| 1 |
{
|
| 2 |
"domain": "plugwise",
|
| 3 |
-
"name": "Plugwise
|
| 4 |
-
"after_dependencies": ["zeroconf"],
|
| 5 |
"codeowners": ["@CoMPaTech", "@bouwew"],
|
| 6 |
"config_flow": true,
|
| 7 |
-
"documentation": "https://
|
| 8 |
"integration_type": "hub",
|
| 9 |
"iot_class": "local_polling",
|
| 10 |
"loggers": ["plugwise"],
|
| 11 |
-
"
|
| 12 |
-
"
|
|
|
|
| 13 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"domain": "plugwise",
|
| 3 |
+
"name": "Plugwise",
|
|
|
|
| 4 |
"codeowners": ["@CoMPaTech", "@bouwew"],
|
| 5 |
"config_flow": true,
|
| 6 |
+
"documentation": "https://www.home-assistant.io/integrations/plugwise",
|
| 7 |
"integration_type": "hub",
|
| 8 |
"iot_class": "local_polling",
|
| 9 |
"loggers": ["plugwise"],
|
| 10 |
+
"quality_scale": "platinum",
|
| 11 |
+
"requirements": ["plugwise==1.10.0"],
|
| 12 |
+
"zeroconf": ["_plugwise._tcp.local."]
|
| 13 |
}
|
|
@@ -1,43 +1,29 @@
|
|
| 1 |
"""Number platform for Plugwise integration."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
-
from collections.abc import Awaitable, Callable
|
| 5 |
from dataclasses import dataclass
|
| 6 |
|
| 7 |
-
from plugwise import Smile
|
| 8 |
-
from plugwise.constants import NumberType
|
| 9 |
-
|
| 10 |
from homeassistant.components.number import (
|
| 11 |
NumberDeviceClass,
|
| 12 |
NumberEntity,
|
| 13 |
NumberEntityDescription,
|
| 14 |
NumberMode,
|
| 15 |
)
|
| 16 |
-
from homeassistant.config_entries import ConfigEntry
|
| 17 |
from homeassistant.const import EntityCategory, UnitOfTemperature
|
| 18 |
-
from homeassistant.core import HomeAssistant
|
| 19 |
-
from homeassistant.helpers.entity_platform import
|
| 20 |
|
| 21 |
-
from .const import
|
| 22 |
-
|
| 23 |
-
DOMAIN,
|
| 24 |
-
LOGGER,
|
| 25 |
-
)
|
| 26 |
-
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 27 |
from .entity import PlugwiseEntity
|
|
|
|
| 28 |
|
|
|
|
| 29 |
|
| 30 |
-
@dataclass
|
| 31 |
-
class PlugwiseEntityDescriptionMixin:
|
| 32 |
-
"""Mixin values for Plugwise entities."""
|
| 33 |
-
|
| 34 |
-
command: Callable[[Smile, str, str, float], Awaitable[None]]
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
class PlugwiseNumberEntityDescription(
|
| 39 |
-
NumberEntityDescription, PlugwiseEntityDescriptionMixin
|
| 40 |
-
):
|
| 41 |
"""Class describing Plugwise Number entities."""
|
| 42 |
|
| 43 |
key: NumberType
|
|
@@ -47,9 +33,6 @@
|
|
| 47 |
PlugwiseNumberEntityDescription(
|
| 48 |
key="maximum_boiler_temperature",
|
| 49 |
translation_key="maximum_boiler_temperature",
|
| 50 |
-
command=lambda api, number, dev_id, value: api.set_number_setpoint(
|
| 51 |
-
number, dev_id, value
|
| 52 |
-
),
|
| 53 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 54 |
entity_category=EntityCategory.CONFIG,
|
| 55 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
@@ -57,9 +40,6 @@
|
|
| 57 |
PlugwiseNumberEntityDescription(
|
| 58 |
key="max_dhw_temperature",
|
| 59 |
translation_key="max_dhw_temperature",
|
| 60 |
-
command=lambda api, number, dev_id, value: api.set_number_setpoint(
|
| 61 |
-
number, dev_id, value
|
| 62 |
-
),
|
| 63 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 64 |
entity_category=EntityCategory.CONFIG,
|
| 65 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
@@ -67,9 +47,6 @@
|
|
| 67 |
PlugwiseNumberEntityDescription(
|
| 68 |
key="temperature_offset",
|
| 69 |
translation_key="temperature_offset",
|
| 70 |
-
command=lambda api, number, dev_id, value: api.set_temperature_offset(
|
| 71 |
-
number, dev_id, value
|
| 72 |
-
),
|
| 73 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 74 |
entity_category=EntityCategory.CONFIG,
|
| 75 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
@@ -79,27 +56,27 @@
|
|
| 79 |
|
| 80 |
async def async_setup_entry(
|
| 81 |
hass: HomeAssistant,
|
| 82 |
-
|
| 83 |
-
async_add_entities:
|
| 84 |
) -> None:
|
| 85 |
"""Set up Plugwise number platform."""
|
|
|
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
"Add %s %s number", device["name"], description.translation_key
|
| 100 |
-
)
|
| 101 |
|
| 102 |
-
|
|
|
|
| 103 |
|
| 104 |
|
| 105 |
class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
|
|
@@ -115,13 +92,12 @@
|
|
| 115 |
) -> None:
|
| 116 |
"""Initiate Plugwise Number."""
|
| 117 |
super().__init__(coordinator, device_id)
|
| 118 |
-
self.actuator = self.device[description.key]
|
| 119 |
-
self.device_id = device_id
|
| 120 |
-
self.entity_description = description
|
| 121 |
-
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 122 |
self._attr_mode = NumberMode.BOX
|
| 123 |
self._attr_native_max_value = self.device[description.key]["upper_bound"]
|
| 124 |
self._attr_native_min_value = self.device[description.key]["lower_bound"]
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
native_step = self.device[description.key]["resolution"]
|
| 127 |
if description.key != "temperature_offset":
|
|
@@ -133,12 +109,9 @@
|
|
| 133 |
"""Return the present setpoint value."""
|
| 134 |
return self.device[self.entity_description.key]["setpoint"]
|
| 135 |
|
|
|
|
| 136 |
async def async_set_native_value(self, value: float) -> None:
|
| 137 |
"""Change to the new setpoint value."""
|
| 138 |
-
await self.
|
| 139 |
-
self.
|
| 140 |
-
)
|
| 141 |
-
LOGGER.debug(
|
| 142 |
-
"Setting %s to %s was successful", self.entity_description.name, value
|
| 143 |
)
|
| 144 |
-
await self.coordinator.async_request_refresh()
|
|
|
|
| 1 |
"""Number platform for Plugwise integration."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from dataclasses import dataclass
|
| 6 |
|
|
|
|
|
|
|
|
|
|
| 7 |
from homeassistant.components.number import (
|
| 8 |
NumberDeviceClass,
|
| 9 |
NumberEntity,
|
| 10 |
NumberEntityDescription,
|
| 11 |
NumberMode,
|
| 12 |
)
|
|
|
|
| 13 |
from homeassistant.const import EntityCategory, UnitOfTemperature
|
| 14 |
+
from homeassistant.core import HomeAssistant, callback
|
| 15 |
+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 16 |
|
| 17 |
+
from .const import NumberType
|
| 18 |
+
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
from .entity import PlugwiseEntity
|
| 20 |
+
from .util import plugwise_command
|
| 21 |
|
| 22 |
+
PARALLEL_UPDATES = 0
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
@dataclass(frozen=True, kw_only=True)
|
| 26 |
+
class PlugwiseNumberEntityDescription(NumberEntityDescription):
|
|
|
|
|
|
|
|
|
|
| 27 |
"""Class describing Plugwise Number entities."""
|
| 28 |
|
| 29 |
key: NumberType
|
|
|
|
| 33 |
PlugwiseNumberEntityDescription(
|
| 34 |
key="maximum_boiler_temperature",
|
| 35 |
translation_key="maximum_boiler_temperature",
|
|
|
|
|
|
|
|
|
|
| 36 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 37 |
entity_category=EntityCategory.CONFIG,
|
| 38 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
| 40 |
PlugwiseNumberEntityDescription(
|
| 41 |
key="max_dhw_temperature",
|
| 42 |
translation_key="max_dhw_temperature",
|
|
|
|
|
|
|
|
|
|
| 43 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 44 |
entity_category=EntityCategory.CONFIG,
|
| 45 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
| 47 |
PlugwiseNumberEntityDescription(
|
| 48 |
key="temperature_offset",
|
| 49 |
translation_key="temperature_offset",
|
|
|
|
|
|
|
|
|
|
| 50 |
device_class=NumberDeviceClass.TEMPERATURE,
|
| 51 |
entity_category=EntityCategory.CONFIG,
|
| 52 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
| 56 |
|
| 57 |
async def async_setup_entry(
|
| 58 |
hass: HomeAssistant,
|
| 59 |
+
entry: PlugwiseConfigEntry,
|
| 60 |
+
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 61 |
) -> None:
|
| 62 |
"""Set up Plugwise number platform."""
|
| 63 |
+
coordinator = entry.runtime_data
|
| 64 |
|
| 65 |
+
@callback
|
| 66 |
+
def _add_entities() -> None:
|
| 67 |
+
"""Add Entities."""
|
| 68 |
+
if not coordinator.new_devices:
|
| 69 |
+
return
|
| 70 |
+
|
| 71 |
+
async_add_entities(
|
| 72 |
+
PlugwiseNumberEntity(coordinator, device_id, description)
|
| 73 |
+
for device_id in coordinator.new_devices
|
| 74 |
+
for description in NUMBER_TYPES
|
| 75 |
+
if description.key in coordinator.data[device_id]
|
| 76 |
+
)
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
_add_entities()
|
| 79 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 80 |
|
| 81 |
|
| 82 |
class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
|
|
|
|
| 92 |
) -> None:
|
| 93 |
"""Initiate Plugwise Number."""
|
| 94 |
super().__init__(coordinator, device_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
self._attr_mode = NumberMode.BOX
|
| 96 |
self._attr_native_max_value = self.device[description.key]["upper_bound"]
|
| 97 |
self._attr_native_min_value = self.device[description.key]["lower_bound"]
|
| 98 |
+
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 99 |
+
self.device_id = device_id
|
| 100 |
+
self.entity_description = description
|
| 101 |
|
| 102 |
native_step = self.device[description.key]["resolution"]
|
| 103 |
if description.key != "temperature_offset":
|
|
|
|
| 109 |
"""Return the present setpoint value."""
|
| 110 |
return self.device[self.entity_description.key]["setpoint"]
|
| 111 |
|
| 112 |
+
@plugwise_command
|
| 113 |
async def async_set_native_value(self, value: float) -> None:
|
| 114 |
"""Change to the new setpoint value."""
|
| 115 |
+
await self.coordinator.api.set_number(
|
| 116 |
+
self.device_id, self.entity_description.key, value
|
|
|
|
|
|
|
|
|
|
| 117 |
)
|
|
|
|
@@ -1,95 +1,95 @@
|
|
| 1 |
"""Plugwise Select component for Home Assistant."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
-
from collections.abc import Awaitable, Callable
|
| 5 |
from dataclasses import dataclass
|
| 6 |
|
| 7 |
-
from plugwise import Smile
|
| 8 |
-
from plugwise.constants import SelectOptionsType, SelectType
|
| 9 |
-
|
| 10 |
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
| 11 |
-
from homeassistant.config_entries import ConfigEntry
|
| 12 |
from homeassistant.const import STATE_ON, EntityCategory
|
| 13 |
-
from homeassistant.core import HomeAssistant
|
| 14 |
-
from homeassistant.helpers.entity_platform import
|
| 15 |
|
| 16 |
from .const import (
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
)
|
| 21 |
-
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 22 |
from .entity import PlugwiseEntity
|
|
|
|
| 23 |
|
| 24 |
PARALLEL_UPDATES = 0
|
| 25 |
|
| 26 |
|
| 27 |
-
@dataclass
|
| 28 |
-
class
|
| 29 |
-
"""Mixin values for Plugwise Select entities."""
|
| 30 |
-
|
| 31 |
-
command: Callable[[Smile, str, str], Awaitable[None]]
|
| 32 |
-
options_key: SelectOptionsType
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
@dataclass
|
| 36 |
-
class PlugwiseSelectEntityDescription(
|
| 37 |
-
SelectEntityDescription, PlugwiseSelectDescriptionMixin
|
| 38 |
-
):
|
| 39 |
"""Class describing Plugwise Select entities."""
|
| 40 |
|
| 41 |
key: SelectType
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
SELECT_TYPES = (
|
| 45 |
PlugwiseSelectEntityDescription(
|
| 46 |
-
key=
|
| 47 |
-
translation_key=
|
| 48 |
-
|
| 49 |
-
command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON),
|
| 50 |
options_key="available_schedules",
|
| 51 |
),
|
| 52 |
PlugwiseSelectEntityDescription(
|
| 53 |
-
key=
|
| 54 |
-
translation_key=
|
| 55 |
-
icon="mdi:hvac",
|
| 56 |
entity_category=EntityCategory.CONFIG,
|
| 57 |
-
command=lambda api, loc, opt: api.set_regulation_mode(opt),
|
| 58 |
options_key="regulation_modes",
|
| 59 |
),
|
| 60 |
PlugwiseSelectEntityDescription(
|
| 61 |
-
key=
|
| 62 |
-
translation_key=
|
| 63 |
-
icon="mdi:shower",
|
| 64 |
entity_category=EntityCategory.CONFIG,
|
| 65 |
-
command=lambda api, loc, opt: api.set_dhw_mode(opt),
|
| 66 |
options_key="dhw_modes",
|
| 67 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
)
|
| 69 |
|
| 70 |
|
| 71 |
async def async_setup_entry(
|
| 72 |
hass: HomeAssistant,
|
| 73 |
-
|
| 74 |
-
async_add_entities:
|
| 75 |
) -> None:
|
| 76 |
"""Set up the Smile selector from a config entry."""
|
| 77 |
-
coordinator
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
|
| 92 |
-
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
|
|
@@ -105,23 +105,29 @@
|
|
| 105 |
) -> None:
|
| 106 |
"""Initialise the selector."""
|
| 107 |
super().__init__(coordinator, device_id)
|
| 108 |
-
self.entity_description = entity_description
|
| 109 |
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
| 110 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
@property
|
| 113 |
-
def current_option(self) -> str:
|
| 114 |
"""Return the selected entity option to represent the entity state."""
|
| 115 |
return self.device[self.entity_description.key]
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
async def async_select_option(self, option: str) -> None:
|
| 118 |
-
"""Change to the selected entity option.
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
self.entity_description.name,
|
| 125 |
-
option,
|
| 126 |
)
|
| 127 |
-
await self.coordinator.async_request_refresh()
|
|
|
|
| 1 |
"""Plugwise Select component for Home Assistant."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from dataclasses import dataclass
|
| 6 |
|
|
|
|
|
|
|
|
|
|
| 7 |
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
|
|
|
| 8 |
from homeassistant.const import STATE_ON, EntityCategory
|
| 9 |
+
from homeassistant.core import HomeAssistant, callback
|
| 10 |
+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 11 |
|
| 12 |
from .const import (
|
| 13 |
+
SELECT_DHW_MODE,
|
| 14 |
+
SELECT_GATEWAY_MODE,
|
| 15 |
+
SELECT_REGULATION_MODE,
|
| 16 |
+
SELECT_SCHEDULE,
|
| 17 |
+
SELECT_ZONE_PROFILE,
|
| 18 |
+
SelectOptionsType,
|
| 19 |
+
SelectType,
|
| 20 |
)
|
| 21 |
+
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
| 22 |
from .entity import PlugwiseEntity
|
| 23 |
+
from .util import plugwise_command
|
| 24 |
|
| 25 |
PARALLEL_UPDATES = 0
|
| 26 |
|
| 27 |
|
| 28 |
+
@dataclass(frozen=True, kw_only=True)
|
| 29 |
+
class PlugwiseSelectEntityDescription(SelectEntityDescription):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
"""Class describing Plugwise Select entities."""
|
| 31 |
|
| 32 |
key: SelectType
|
| 33 |
+
options_key: SelectOptionsType
|
| 34 |
|
| 35 |
|
| 36 |
SELECT_TYPES = (
|
| 37 |
PlugwiseSelectEntityDescription(
|
| 38 |
+
key=SELECT_SCHEDULE,
|
| 39 |
+
translation_key=SELECT_SCHEDULE,
|
| 40 |
+
entity_category=EntityCategory.CONFIG,
|
|
|
|
| 41 |
options_key="available_schedules",
|
| 42 |
),
|
| 43 |
PlugwiseSelectEntityDescription(
|
| 44 |
+
key=SELECT_REGULATION_MODE,
|
| 45 |
+
translation_key=SELECT_REGULATION_MODE,
|
|
|
|
| 46 |
entity_category=EntityCategory.CONFIG,
|
|
|
|
| 47 |
options_key="regulation_modes",
|
| 48 |
),
|
| 49 |
PlugwiseSelectEntityDescription(
|
| 50 |
+
key=SELECT_DHW_MODE,
|
| 51 |
+
translation_key=SELECT_DHW_MODE,
|
|
|
|
| 52 |
entity_category=EntityCategory.CONFIG,
|
|
|
|
| 53 |
options_key="dhw_modes",
|
| 54 |
),
|
| 55 |
+
PlugwiseSelectEntityDescription(
|
| 56 |
+
key=SELECT_GATEWAY_MODE,
|
| 57 |
+
translation_key=SELECT_GATEWAY_MODE,
|
| 58 |
+
entity_category=EntityCategory.CONFIG,
|
| 59 |
+
options_key="gateway_modes",
|
| 60 |
+
),
|
| 61 |
+
PlugwiseSelectEntityDescription(
|
| 62 |
+
key=SELECT_ZONE_PROFILE,
|
| 63 |
+
translation_key=SELECT_ZONE_PROFILE,
|
| 64 |
+
entity_category=EntityCategory.CONFIG,
|
| 65 |
+
options_key="zone_profiles",
|
| 66 |
+
),
|
| 67 |
)
|
| 68 |
|
| 69 |
|
| 70 |
async def async_setup_entry(
|
| 71 |
hass: HomeAssistant,
|
| 72 |
+
entry: PlugwiseConfigEntry,
|
| 73 |
+
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 74 |
) -> None:
|
| 75 |
"""Set up the Smile selector from a config entry."""
|
| 76 |
+
coordinator = entry.runtime_data
|
| 77 |
+
|
| 78 |
+
@callback
|
| 79 |
+
def _add_entities() -> None:
|
| 80 |
+
"""Add Entities."""
|
| 81 |
+
if not coordinator.new_devices:
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
async_add_entities(
|
| 85 |
+
PlugwiseSelectEntity(coordinator, device_id, description)
|
| 86 |
+
for device_id in coordinator.new_devices
|
| 87 |
+
for description in SELECT_TYPES
|
| 88 |
+
if coordinator.data[device_id].get(description.options_key)
|
| 89 |
+
)
|
| 90 |
|
| 91 |
+
_add_entities()
|
| 92 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 93 |
|
| 94 |
|
| 95 |
class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
|
|
|
|
| 105 |
) -> None:
|
| 106 |
"""Initialise the selector."""
|
| 107 |
super().__init__(coordinator, device_id)
|
|
|
|
| 108 |
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
| 109 |
+
self.entity_description = entity_description
|
| 110 |
+
|
| 111 |
+
self._location = device_id
|
| 112 |
+
if (location := self.device.get("location")) is not None:
|
| 113 |
+
self._location = location
|
| 114 |
|
| 115 |
@property
|
| 116 |
+
def current_option(self) -> str | None:
|
| 117 |
"""Return the selected entity option to represent the entity state."""
|
| 118 |
return self.device[self.entity_description.key]
|
| 119 |
|
| 120 |
+
@property
|
| 121 |
+
def options(self) -> list[str]:
|
| 122 |
+
"""Return the available select-options."""
|
| 123 |
+
return self.device[self.entity_description.options_key]
|
| 124 |
+
|
| 125 |
+
@plugwise_command
|
| 126 |
async def async_select_option(self, option: str) -> None:
|
| 127 |
+
"""Change to the selected entity option.
|
| 128 |
+
|
| 129 |
+
self._location and STATE_ON are required for the thermostat-schedule select.
|
| 130 |
+
"""
|
| 131 |
+
await self.coordinator.api.set_select(
|
| 132 |
+
self.entity_description.key, self._location, option, STATE_ON
|
|
|
|
|
|
|
| 133 |
)
|
|
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
"""Plugwise Sensor component for Home Assistant."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
from dataclasses import dataclass
|
|
@@ -11,7 +12,6 @@
|
|
| 11 |
SensorEntityDescription,
|
| 12 |
SensorStateClass,
|
| 13 |
)
|
| 14 |
-
from homeassistant.config_entries import ConfigEntry
|
| 15 |
from homeassistant.const import (
|
| 16 |
LIGHT_LUX,
|
| 17 |
PERCENTAGE,
|
|
@@ -24,184 +24,190 @@
|
|
| 24 |
UnitOfVolume,
|
| 25 |
UnitOfVolumeFlowRate,
|
| 26 |
)
|
| 27 |
-
from homeassistant.core import HomeAssistant
|
| 28 |
-
from homeassistant.helpers.entity_platform import
|
| 29 |
|
| 30 |
-
from .
|
| 31 |
-
COORDINATOR, # pw-beta
|
| 32 |
-
DOMAIN,
|
| 33 |
-
LOGGER,
|
| 34 |
-
)
|
| 35 |
-
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 36 |
from .entity import PlugwiseEntity
|
| 37 |
|
|
|
|
| 38 |
PARALLEL_UPDATES = 0
|
| 39 |
|
| 40 |
|
| 41 |
-
@dataclass
|
| 42 |
class PlugwiseSensorEntityDescription(SensorEntityDescription):
|
| 43 |
"""Describes Plugwise sensor entity."""
|
| 44 |
|
| 45 |
key: SensorType
|
| 46 |
-
state_class: str | None = SensorStateClass.MEASUREMENT
|
| 47 |
|
| 48 |
|
| 49 |
SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
|
| 50 |
PlugwiseSensorEntityDescription(
|
| 51 |
key="setpoint",
|
| 52 |
translation_key="setpoint",
|
| 53 |
-
device_class=SensorDeviceClass.TEMPERATURE,
|
| 54 |
-
entity_category=EntityCategory.DIAGNOSTIC,
|
| 55 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
|
|
|
| 56 |
),
|
| 57 |
PlugwiseSensorEntityDescription(
|
| 58 |
key="setpoint_high",
|
| 59 |
translation_key="cooling_setpoint",
|
| 60 |
-
device_class=SensorDeviceClass.TEMPERATURE,
|
| 61 |
-
entity_category=EntityCategory.DIAGNOSTIC,
|
| 62 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
|
|
|
| 63 |
),
|
| 64 |
PlugwiseSensorEntityDescription(
|
| 65 |
key="setpoint_low",
|
| 66 |
translation_key="heating_setpoint",
|
| 67 |
-
device_class=SensorDeviceClass.TEMPERATURE,
|
| 68 |
-
entity_category=EntityCategory.DIAGNOSTIC,
|
| 69 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
|
|
|
| 70 |
),
|
| 71 |
PlugwiseSensorEntityDescription(
|
| 72 |
key="temperature",
|
| 73 |
-
device_class=SensorDeviceClass.TEMPERATURE,
|
| 74 |
-
entity_category=EntityCategory.DIAGNOSTIC,
|
| 75 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
|
|
|
| 76 |
),
|
| 77 |
PlugwiseSensorEntityDescription(
|
| 78 |
key="intended_boiler_temperature",
|
| 79 |
translation_key="intended_boiler_temperature",
|
|
|
|
| 80 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 81 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 82 |
-
|
| 83 |
),
|
| 84 |
PlugwiseSensorEntityDescription(
|
| 85 |
key="temperature_difference",
|
| 86 |
translation_key="temperature_difference",
|
| 87 |
-
entity_category=EntityCategory.DIAGNOSTIC,
|
| 88 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 89 |
-
|
| 90 |
-
PlugwiseSensorEntityDescription(
|
| 91 |
-
key="uncorrected_temperature",
|
| 92 |
-
translation_key="uncorrected_temperature",
|
| 93 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 94 |
-
|
| 95 |
),
|
| 96 |
PlugwiseSensorEntityDescription(
|
| 97 |
key="outdoor_temperature",
|
| 98 |
translation_key="outdoor_temperature",
|
| 99 |
-
device_class=SensorDeviceClass.TEMPERATURE,
|
| 100 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 101 |
-
|
|
|
|
|
|
|
| 102 |
),
|
| 103 |
PlugwiseSensorEntityDescription(
|
| 104 |
key="outdoor_air_temperature",
|
| 105 |
translation_key="outdoor_air_temperature",
|
|
|
|
| 106 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 107 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 108 |
-
|
| 109 |
-
suggested_display_precision=1,
|
| 110 |
),
|
| 111 |
PlugwiseSensorEntityDescription(
|
| 112 |
key="water_temperature",
|
| 113 |
translation_key="water_temperature",
|
|
|
|
| 114 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 115 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 116 |
-
|
| 117 |
),
|
| 118 |
PlugwiseSensorEntityDescription(
|
| 119 |
key="return_temperature",
|
| 120 |
translation_key="return_temperature",
|
|
|
|
| 121 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 122 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 123 |
-
|
| 124 |
),
|
| 125 |
PlugwiseSensorEntityDescription(
|
| 126 |
key="electricity_consumed",
|
| 127 |
translation_key="electricity_consumed",
|
| 128 |
-
device_class=SensorDeviceClass.POWER,
|
| 129 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
|
|
|
| 130 |
),
|
| 131 |
PlugwiseSensorEntityDescription(
|
| 132 |
key="electricity_produced",
|
| 133 |
translation_key="electricity_produced",
|
| 134 |
-
device_class=SensorDeviceClass.POWER,
|
| 135 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
|
|
|
| 136 |
entity_registry_enabled_default=False,
|
| 137 |
),
|
| 138 |
PlugwiseSensorEntityDescription(
|
| 139 |
key="electricity_consumed_interval",
|
| 140 |
translation_key="electricity_consumed_interval",
|
| 141 |
-
icon="mdi:lightning-bolt",
|
| 142 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
|
|
|
|
|
| 143 |
),
|
| 144 |
PlugwiseSensorEntityDescription(
|
| 145 |
key="electricity_consumed_peak_interval",
|
| 146 |
translation_key="electricity_consumed_peak_interval",
|
| 147 |
-
icon="mdi:lightning-bolt",
|
| 148 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
|
|
|
|
|
| 149 |
),
|
| 150 |
PlugwiseSensorEntityDescription(
|
| 151 |
key="electricity_consumed_off_peak_interval",
|
| 152 |
translation_key="electricity_consumed_off_peak_interval",
|
| 153 |
-
icon="mdi:lightning-bolt",
|
| 154 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
|
|
|
|
|
| 155 |
),
|
| 156 |
PlugwiseSensorEntityDescription(
|
| 157 |
key="electricity_produced_interval",
|
| 158 |
translation_key="electricity_produced_interval",
|
| 159 |
-
icon="mdi:lightning-bolt",
|
| 160 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
|
|
|
|
|
| 161 |
entity_registry_enabled_default=False,
|
| 162 |
),
|
| 163 |
PlugwiseSensorEntityDescription(
|
| 164 |
key="electricity_produced_peak_interval",
|
| 165 |
translation_key="electricity_produced_peak_interval",
|
| 166 |
-
icon="mdi:lightning-bolt",
|
| 167 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
|
|
|
|
|
| 168 |
),
|
| 169 |
PlugwiseSensorEntityDescription(
|
| 170 |
key="electricity_produced_off_peak_interval",
|
| 171 |
translation_key="electricity_produced_off_peak_interval",
|
| 172 |
-
icon="mdi:lightning-bolt",
|
| 173 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
|
|
|
|
|
| 174 |
),
|
| 175 |
PlugwiseSensorEntityDescription(
|
| 176 |
key="electricity_consumed_point",
|
| 177 |
translation_key="electricity_consumed_point",
|
| 178 |
device_class=SensorDeviceClass.POWER,
|
| 179 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
| 180 |
),
|
| 181 |
PlugwiseSensorEntityDescription(
|
| 182 |
key="electricity_consumed_off_peak_point",
|
| 183 |
translation_key="electricity_consumed_off_peak_point",
|
| 184 |
-
device_class=SensorDeviceClass.POWER,
|
| 185 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
|
|
|
| 186 |
),
|
| 187 |
PlugwiseSensorEntityDescription(
|
| 188 |
key="electricity_consumed_peak_point",
|
| 189 |
translation_key="electricity_consumed_peak_point",
|
| 190 |
-
device_class=SensorDeviceClass.POWER,
|
| 191 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
|
|
|
| 192 |
),
|
| 193 |
PlugwiseSensorEntityDescription(
|
| 194 |
key="electricity_consumed_off_peak_cumulative",
|
| 195 |
translation_key="electricity_consumed_off_peak_cumulative",
|
| 196 |
-
device_class=SensorDeviceClass.ENERGY,
|
| 197 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
|
|
| 198 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 199 |
),
|
| 200 |
PlugwiseSensorEntityDescription(
|
| 201 |
key="electricity_consumed_peak_cumulative",
|
| 202 |
translation_key="electricity_consumed_peak_cumulative",
|
| 203 |
-
device_class=SensorDeviceClass.ENERGY,
|
| 204 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
|
|
| 205 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 206 |
),
|
| 207 |
PlugwiseSensorEntityDescription(
|
|
@@ -209,24 +215,27 @@
|
|
| 209 |
translation_key="electricity_produced_point",
|
| 210 |
device_class=SensorDeviceClass.POWER,
|
| 211 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
| 212 |
),
|
| 213 |
PlugwiseSensorEntityDescription(
|
| 214 |
key="electricity_produced_off_peak_point",
|
| 215 |
translation_key="electricity_produced_off_peak_point",
|
| 216 |
-
device_class=SensorDeviceClass.POWER,
|
| 217 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
|
|
|
| 218 |
),
|
| 219 |
PlugwiseSensorEntityDescription(
|
| 220 |
key="electricity_produced_peak_point",
|
| 221 |
translation_key="electricity_produced_peak_point",
|
| 222 |
-
device_class=SensorDeviceClass.POWER,
|
| 223 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
|
|
|
| 224 |
),
|
| 225 |
PlugwiseSensorEntityDescription(
|
| 226 |
key="electricity_produced_off_peak_cumulative",
|
| 227 |
translation_key="electricity_produced_off_peak_cumulative",
|
| 228 |
-
device_class=SensorDeviceClass.ENERGY,
|
| 229 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
|
|
| 230 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 231 |
),
|
| 232 |
PlugwiseSensorEntityDescription(
|
|
@@ -239,45 +248,51 @@
|
|
| 239 |
PlugwiseSensorEntityDescription(
|
| 240 |
key="electricity_phase_one_consumed",
|
| 241 |
translation_key="electricity_phase_one_consumed",
|
| 242 |
-
name="Electricity phase one consumed",
|
| 243 |
device_class=SensorDeviceClass.POWER,
|
| 244 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
| 245 |
),
|
| 246 |
PlugwiseSensorEntityDescription(
|
| 247 |
key="electricity_phase_two_consumed",
|
| 248 |
translation_key="electricity_phase_two_consumed",
|
| 249 |
device_class=SensorDeviceClass.POWER,
|
| 250 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
| 251 |
),
|
| 252 |
PlugwiseSensorEntityDescription(
|
| 253 |
key="electricity_phase_three_consumed",
|
| 254 |
translation_key="electricity_phase_three_consumed",
|
| 255 |
device_class=SensorDeviceClass.POWER,
|
| 256 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
| 257 |
),
|
| 258 |
PlugwiseSensorEntityDescription(
|
| 259 |
key="electricity_phase_one_produced",
|
| 260 |
translation_key="electricity_phase_one_produced",
|
| 261 |
device_class=SensorDeviceClass.POWER,
|
| 262 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
| 263 |
),
|
| 264 |
PlugwiseSensorEntityDescription(
|
| 265 |
key="electricity_phase_two_produced",
|
| 266 |
translation_key="electricity_phase_two_produced",
|
| 267 |
device_class=SensorDeviceClass.POWER,
|
| 268 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
| 269 |
),
|
| 270 |
PlugwiseSensorEntityDescription(
|
| 271 |
key="electricity_phase_three_produced",
|
| 272 |
translation_key="electricity_phase_three_produced",
|
| 273 |
device_class=SensorDeviceClass.POWER,
|
| 274 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
| 275 |
),
|
| 276 |
PlugwiseSensorEntityDescription(
|
| 277 |
key="voltage_phase_one",
|
| 278 |
translation_key="voltage_phase_one",
|
| 279 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 280 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
|
|
| 281 |
entity_registry_enabled_default=False,
|
| 282 |
),
|
| 283 |
PlugwiseSensorEntityDescription(
|
|
@@ -285,6 +300,7 @@
|
|
| 285 |
translation_key="voltage_phase_two",
|
| 286 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 287 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
|
|
| 288 |
entity_registry_enabled_default=False,
|
| 289 |
),
|
| 290 |
PlugwiseSensorEntityDescription(
|
|
@@ -292,116 +308,121 @@
|
|
| 292 |
translation_key="voltage_phase_three",
|
| 293 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 294 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
|
|
| 295 |
entity_registry_enabled_default=False,
|
| 296 |
),
|
| 297 |
PlugwiseSensorEntityDescription(
|
| 298 |
key="gas_consumed_interval",
|
| 299 |
translation_key="gas_consumed_interval",
|
| 300 |
-
icon="mdi:meter-gas",
|
| 301 |
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
|
|
|
| 302 |
),
|
| 303 |
PlugwiseSensorEntityDescription(
|
| 304 |
key="gas_consumed_cumulative",
|
| 305 |
translation_key="gas_consumed_cumulative",
|
| 306 |
-
device_class=SensorDeviceClass.GAS,
|
| 307 |
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
| 308 |
-
|
|
|
|
| 309 |
),
|
| 310 |
PlugwiseSensorEntityDescription(
|
| 311 |
key="net_electricity_point",
|
| 312 |
translation_key="net_electricity_point",
|
| 313 |
-
device_class=SensorDeviceClass.POWER,
|
| 314 |
native_unit_of_measurement=UnitOfPower.WATT,
|
|
|
|
|
|
|
| 315 |
),
|
| 316 |
PlugwiseSensorEntityDescription(
|
| 317 |
key="net_electricity_cumulative",
|
| 318 |
translation_key="net_electricity_cumulative",
|
| 319 |
-
device_class=SensorDeviceClass.ENERGY,
|
| 320 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
|
|
| 321 |
state_class=SensorStateClass.TOTAL,
|
| 322 |
),
|
| 323 |
PlugwiseSensorEntityDescription(
|
| 324 |
key="battery",
|
|
|
|
| 325 |
device_class=SensorDeviceClass.BATTERY,
|
| 326 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 327 |
-
|
| 328 |
),
|
| 329 |
PlugwiseSensorEntityDescription(
|
| 330 |
key="illuminance",
|
|
|
|
| 331 |
device_class=SensorDeviceClass.ILLUMINANCE,
|
| 332 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 333 |
-
|
| 334 |
),
|
| 335 |
PlugwiseSensorEntityDescription(
|
| 336 |
key="modulation_level",
|
| 337 |
translation_key="modulation_level",
|
| 338 |
-
entity_category=EntityCategory.DIAGNOSTIC,
|
| 339 |
native_unit_of_measurement=PERCENTAGE,
|
| 340 |
-
|
|
|
|
| 341 |
),
|
| 342 |
PlugwiseSensorEntityDescription(
|
| 343 |
key="valve_position",
|
| 344 |
translation_key="valve_position",
|
| 345 |
-
icon="mdi:valve",
|
| 346 |
-
entity_category=EntityCategory.DIAGNOSTIC,
|
| 347 |
native_unit_of_measurement=PERCENTAGE,
|
|
|
|
|
|
|
| 348 |
),
|
| 349 |
PlugwiseSensorEntityDescription(
|
| 350 |
key="water_pressure",
|
| 351 |
translation_key="water_pressure",
|
|
|
|
| 352 |
device_class=SensorDeviceClass.PRESSURE,
|
| 353 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 354 |
-
|
| 355 |
),
|
| 356 |
PlugwiseSensorEntityDescription(
|
| 357 |
key="humidity",
|
| 358 |
-
device_class=SensorDeviceClass.HUMIDITY,
|
| 359 |
native_unit_of_measurement=PERCENTAGE,
|
|
|
|
|
|
|
| 360 |
),
|
| 361 |
PlugwiseSensorEntityDescription(
|
| 362 |
key="dhw_temperature",
|
| 363 |
translation_key="dhw_temperature",
|
|
|
|
| 364 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 365 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 366 |
-
|
| 367 |
),
|
| 368 |
PlugwiseSensorEntityDescription(
|
| 369 |
key="domestic_hot_water_setpoint",
|
| 370 |
translation_key="domestic_hot_water_setpoint",
|
|
|
|
| 371 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 372 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 373 |
-
|
| 374 |
),
|
| 375 |
)
|
| 376 |
|
| 377 |
|
| 378 |
async def async_setup_entry(
|
| 379 |
hass: HomeAssistant,
|
| 380 |
-
|
| 381 |
-
async_add_entities:
|
| 382 |
) -> None:
|
| 383 |
"""Set up the Smile sensors from a config entry."""
|
| 384 |
-
coordinator =
|
| 385 |
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
)
|
| 400 |
-
LOGGER.debug(
|
| 401 |
-
"Add %s %s sensor", device["name"], description.translation_key
|
| 402 |
-
)
|
| 403 |
|
| 404 |
-
|
|
|
|
| 405 |
|
| 406 |
|
| 407 |
class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
|
|
@@ -417,8 +438,8 @@
|
|
| 417 |
) -> None:
|
| 418 |
"""Initialise the sensor."""
|
| 419 |
super().__init__(coordinator, device_id)
|
| 420 |
-
self.entity_description = description
|
| 421 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
|
|
|
| 422 |
|
| 423 |
@property
|
| 424 |
def native_value(self) -> int | float:
|
|
|
|
| 1 |
"""Plugwise Sensor component for Home Assistant."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from dataclasses import dataclass
|
|
|
|
| 12 |
SensorEntityDescription,
|
| 13 |
SensorStateClass,
|
| 14 |
)
|
|
|
|
| 15 |
from homeassistant.const import (
|
| 16 |
LIGHT_LUX,
|
| 17 |
PERCENTAGE,
|
|
|
|
| 24 |
UnitOfVolume,
|
| 25 |
UnitOfVolumeFlowRate,
|
| 26 |
)
|
| 27 |
+
from homeassistant.core import HomeAssistant, callback
|
| 28 |
+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 29 |
|
| 30 |
+
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
from .entity import PlugwiseEntity
|
| 32 |
|
| 33 |
+
# Coordinator is used to centralize the data updates
|
| 34 |
PARALLEL_UPDATES = 0
|
| 35 |
|
| 36 |
|
| 37 |
+
@dataclass(frozen=True)
|
| 38 |
class PlugwiseSensorEntityDescription(SensorEntityDescription):
|
| 39 |
"""Describes Plugwise sensor entity."""
|
| 40 |
|
| 41 |
key: SensorType
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
|
| 45 |
PlugwiseSensorEntityDescription(
|
| 46 |
key="setpoint",
|
| 47 |
translation_key="setpoint",
|
|
|
|
|
|
|
| 48 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 49 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
| 50 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 51 |
),
|
| 52 |
PlugwiseSensorEntityDescription(
|
| 53 |
key="setpoint_high",
|
| 54 |
translation_key="cooling_setpoint",
|
|
|
|
|
|
|
| 55 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 56 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
| 57 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 58 |
),
|
| 59 |
PlugwiseSensorEntityDescription(
|
| 60 |
key="setpoint_low",
|
| 61 |
translation_key="heating_setpoint",
|
|
|
|
|
|
|
| 62 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 63 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
| 64 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 65 |
),
|
| 66 |
PlugwiseSensorEntityDescription(
|
| 67 |
key="temperature",
|
|
|
|
|
|
|
| 68 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 69 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
| 70 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 71 |
),
|
| 72 |
PlugwiseSensorEntityDescription(
|
| 73 |
key="intended_boiler_temperature",
|
| 74 |
translation_key="intended_boiler_temperature",
|
| 75 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 76 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 77 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 78 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 79 |
),
|
| 80 |
PlugwiseSensorEntityDescription(
|
| 81 |
key="temperature_difference",
|
| 82 |
translation_key="temperature_difference",
|
|
|
|
| 83 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 84 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
|
|
|
|
|
|
|
|
|
| 85 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 86 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 87 |
),
|
| 88 |
PlugwiseSensorEntityDescription(
|
| 89 |
key="outdoor_temperature",
|
| 90 |
translation_key="outdoor_temperature",
|
|
|
|
| 91 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 92 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
| 93 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
| 94 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 95 |
),
|
| 96 |
PlugwiseSensorEntityDescription(
|
| 97 |
key="outdoor_air_temperature",
|
| 98 |
translation_key="outdoor_air_temperature",
|
| 99 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 100 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 101 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 102 |
+
state_class=SensorStateClass.MEASUREMENT,
|
|
|
|
| 103 |
),
|
| 104 |
PlugwiseSensorEntityDescription(
|
| 105 |
key="water_temperature",
|
| 106 |
translation_key="water_temperature",
|
| 107 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 108 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 109 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 110 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 111 |
),
|
| 112 |
PlugwiseSensorEntityDescription(
|
| 113 |
key="return_temperature",
|
| 114 |
translation_key="return_temperature",
|
| 115 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 116 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 117 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 118 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 119 |
),
|
| 120 |
PlugwiseSensorEntityDescription(
|
| 121 |
key="electricity_consumed",
|
| 122 |
translation_key="electricity_consumed",
|
|
|
|
| 123 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 124 |
+
device_class=SensorDeviceClass.POWER,
|
| 125 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 126 |
),
|
| 127 |
PlugwiseSensorEntityDescription(
|
| 128 |
key="electricity_produced",
|
| 129 |
translation_key="electricity_produced",
|
|
|
|
| 130 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 131 |
+
device_class=SensorDeviceClass.POWER,
|
| 132 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 133 |
entity_registry_enabled_default=False,
|
| 134 |
),
|
| 135 |
PlugwiseSensorEntityDescription(
|
| 136 |
key="electricity_consumed_interval",
|
| 137 |
translation_key="electricity_consumed_interval",
|
|
|
|
| 138 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 139 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 140 |
+
state_class=SensorStateClass.TOTAL,
|
| 141 |
),
|
| 142 |
PlugwiseSensorEntityDescription(
|
| 143 |
key="electricity_consumed_peak_interval",
|
| 144 |
translation_key="electricity_consumed_peak_interval",
|
|
|
|
| 145 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 146 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 147 |
+
state_class=SensorStateClass.TOTAL,
|
| 148 |
),
|
| 149 |
PlugwiseSensorEntityDescription(
|
| 150 |
key="electricity_consumed_off_peak_interval",
|
| 151 |
translation_key="electricity_consumed_off_peak_interval",
|
|
|
|
| 152 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 153 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 154 |
+
state_class=SensorStateClass.TOTAL,
|
| 155 |
),
|
| 156 |
PlugwiseSensorEntityDescription(
|
| 157 |
key="electricity_produced_interval",
|
| 158 |
translation_key="electricity_produced_interval",
|
|
|
|
| 159 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 160 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 161 |
+
state_class=SensorStateClass.TOTAL,
|
| 162 |
entity_registry_enabled_default=False,
|
| 163 |
),
|
| 164 |
PlugwiseSensorEntityDescription(
|
| 165 |
key="electricity_produced_peak_interval",
|
| 166 |
translation_key="electricity_produced_peak_interval",
|
|
|
|
| 167 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 168 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 169 |
+
state_class=SensorStateClass.TOTAL,
|
| 170 |
),
|
| 171 |
PlugwiseSensorEntityDescription(
|
| 172 |
key="electricity_produced_off_peak_interval",
|
| 173 |
translation_key="electricity_produced_off_peak_interval",
|
|
|
|
| 174 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
| 175 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 176 |
+
state_class=SensorStateClass.TOTAL,
|
| 177 |
),
|
| 178 |
PlugwiseSensorEntityDescription(
|
| 179 |
key="electricity_consumed_point",
|
| 180 |
translation_key="electricity_consumed_point",
|
| 181 |
device_class=SensorDeviceClass.POWER,
|
| 182 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 183 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 184 |
),
|
| 185 |
PlugwiseSensorEntityDescription(
|
| 186 |
key="electricity_consumed_off_peak_point",
|
| 187 |
translation_key="electricity_consumed_off_peak_point",
|
|
|
|
| 188 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 189 |
+
device_class=SensorDeviceClass.POWER,
|
| 190 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 191 |
),
|
| 192 |
PlugwiseSensorEntityDescription(
|
| 193 |
key="electricity_consumed_peak_point",
|
| 194 |
translation_key="electricity_consumed_peak_point",
|
|
|
|
| 195 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 196 |
+
device_class=SensorDeviceClass.POWER,
|
| 197 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 198 |
),
|
| 199 |
PlugwiseSensorEntityDescription(
|
| 200 |
key="electricity_consumed_off_peak_cumulative",
|
| 201 |
translation_key="electricity_consumed_off_peak_cumulative",
|
|
|
|
| 202 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 203 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 204 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 205 |
),
|
| 206 |
PlugwiseSensorEntityDescription(
|
| 207 |
key="electricity_consumed_peak_cumulative",
|
| 208 |
translation_key="electricity_consumed_peak_cumulative",
|
|
|
|
| 209 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 210 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 211 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 212 |
),
|
| 213 |
PlugwiseSensorEntityDescription(
|
|
|
|
| 215 |
translation_key="electricity_produced_point",
|
| 216 |
device_class=SensorDeviceClass.POWER,
|
| 217 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 218 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 219 |
),
|
| 220 |
PlugwiseSensorEntityDescription(
|
| 221 |
key="electricity_produced_off_peak_point",
|
| 222 |
translation_key="electricity_produced_off_peak_point",
|
|
|
|
| 223 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 224 |
+
device_class=SensorDeviceClass.POWER,
|
| 225 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 226 |
),
|
| 227 |
PlugwiseSensorEntityDescription(
|
| 228 |
key="electricity_produced_peak_point",
|
| 229 |
translation_key="electricity_produced_peak_point",
|
|
|
|
| 230 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 231 |
+
device_class=SensorDeviceClass.POWER,
|
| 232 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 233 |
),
|
| 234 |
PlugwiseSensorEntityDescription(
|
| 235 |
key="electricity_produced_off_peak_cumulative",
|
| 236 |
translation_key="electricity_produced_off_peak_cumulative",
|
|
|
|
| 237 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 238 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 239 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
| 240 |
),
|
| 241 |
PlugwiseSensorEntityDescription(
|
|
|
|
| 248 |
PlugwiseSensorEntityDescription(
|
| 249 |
key="electricity_phase_one_consumed",
|
| 250 |
translation_key="electricity_phase_one_consumed",
|
|
|
|
| 251 |
device_class=SensorDeviceClass.POWER,
|
| 252 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 253 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 254 |
),
|
| 255 |
PlugwiseSensorEntityDescription(
|
| 256 |
key="electricity_phase_two_consumed",
|
| 257 |
translation_key="electricity_phase_two_consumed",
|
| 258 |
device_class=SensorDeviceClass.POWER,
|
| 259 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 260 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 261 |
),
|
| 262 |
PlugwiseSensorEntityDescription(
|
| 263 |
key="electricity_phase_three_consumed",
|
| 264 |
translation_key="electricity_phase_three_consumed",
|
| 265 |
device_class=SensorDeviceClass.POWER,
|
| 266 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 267 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 268 |
),
|
| 269 |
PlugwiseSensorEntityDescription(
|
| 270 |
key="electricity_phase_one_produced",
|
| 271 |
translation_key="electricity_phase_one_produced",
|
| 272 |
device_class=SensorDeviceClass.POWER,
|
| 273 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 274 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 275 |
),
|
| 276 |
PlugwiseSensorEntityDescription(
|
| 277 |
key="electricity_phase_two_produced",
|
| 278 |
translation_key="electricity_phase_two_produced",
|
| 279 |
device_class=SensorDeviceClass.POWER,
|
| 280 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 281 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 282 |
),
|
| 283 |
PlugwiseSensorEntityDescription(
|
| 284 |
key="electricity_phase_three_produced",
|
| 285 |
translation_key="electricity_phase_three_produced",
|
| 286 |
device_class=SensorDeviceClass.POWER,
|
| 287 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 288 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 289 |
),
|
| 290 |
PlugwiseSensorEntityDescription(
|
| 291 |
key="voltage_phase_one",
|
| 292 |
translation_key="voltage_phase_one",
|
| 293 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 294 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
| 295 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 296 |
entity_registry_enabled_default=False,
|
| 297 |
),
|
| 298 |
PlugwiseSensorEntityDescription(
|
|
|
|
| 300 |
translation_key="voltage_phase_two",
|
| 301 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 302 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
| 303 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 304 |
entity_registry_enabled_default=False,
|
| 305 |
),
|
| 306 |
PlugwiseSensorEntityDescription(
|
|
|
|
| 308 |
translation_key="voltage_phase_three",
|
| 309 |
device_class=SensorDeviceClass.VOLTAGE,
|
| 310 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
| 311 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 312 |
entity_registry_enabled_default=False,
|
| 313 |
),
|
| 314 |
PlugwiseSensorEntityDescription(
|
| 315 |
key="gas_consumed_interval",
|
| 316 |
translation_key="gas_consumed_interval",
|
|
|
|
| 317 |
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
| 318 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 319 |
),
|
| 320 |
PlugwiseSensorEntityDescription(
|
| 321 |
key="gas_consumed_cumulative",
|
| 322 |
translation_key="gas_consumed_cumulative",
|
|
|
|
| 323 |
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
| 324 |
+
device_class=SensorDeviceClass.GAS,
|
| 325 |
+
state_class=SensorStateClass.TOTAL,
|
| 326 |
),
|
| 327 |
PlugwiseSensorEntityDescription(
|
| 328 |
key="net_electricity_point",
|
| 329 |
translation_key="net_electricity_point",
|
|
|
|
| 330 |
native_unit_of_measurement=UnitOfPower.WATT,
|
| 331 |
+
device_class=SensorDeviceClass.POWER,
|
| 332 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 333 |
),
|
| 334 |
PlugwiseSensorEntityDescription(
|
| 335 |
key="net_electricity_cumulative",
|
| 336 |
translation_key="net_electricity_cumulative",
|
|
|
|
| 337 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
| 338 |
+
device_class=SensorDeviceClass.ENERGY,
|
| 339 |
state_class=SensorStateClass.TOTAL,
|
| 340 |
),
|
| 341 |
PlugwiseSensorEntityDescription(
|
| 342 |
key="battery",
|
| 343 |
+
native_unit_of_measurement=PERCENTAGE,
|
| 344 |
device_class=SensorDeviceClass.BATTERY,
|
| 345 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 346 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 347 |
),
|
| 348 |
PlugwiseSensorEntityDescription(
|
| 349 |
key="illuminance",
|
| 350 |
+
native_unit_of_measurement=LIGHT_LUX,
|
| 351 |
device_class=SensorDeviceClass.ILLUMINANCE,
|
| 352 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 353 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 354 |
),
|
| 355 |
PlugwiseSensorEntityDescription(
|
| 356 |
key="modulation_level",
|
| 357 |
translation_key="modulation_level",
|
|
|
|
| 358 |
native_unit_of_measurement=PERCENTAGE,
|
| 359 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
| 360 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 361 |
),
|
| 362 |
PlugwiseSensorEntityDescription(
|
| 363 |
key="valve_position",
|
| 364 |
translation_key="valve_position",
|
|
|
|
|
|
|
| 365 |
native_unit_of_measurement=PERCENTAGE,
|
| 366 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
| 367 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 368 |
),
|
| 369 |
PlugwiseSensorEntityDescription(
|
| 370 |
key="water_pressure",
|
| 371 |
translation_key="water_pressure",
|
| 372 |
+
native_unit_of_measurement=UnitOfPressure.BAR,
|
| 373 |
device_class=SensorDeviceClass.PRESSURE,
|
| 374 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 375 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 376 |
),
|
| 377 |
PlugwiseSensorEntityDescription(
|
| 378 |
key="humidity",
|
|
|
|
| 379 |
native_unit_of_measurement=PERCENTAGE,
|
| 380 |
+
device_class=SensorDeviceClass.HUMIDITY,
|
| 381 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 382 |
),
|
| 383 |
PlugwiseSensorEntityDescription(
|
| 384 |
key="dhw_temperature",
|
| 385 |
translation_key="dhw_temperature",
|
| 386 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 387 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 388 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 389 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 390 |
),
|
| 391 |
PlugwiseSensorEntityDescription(
|
| 392 |
key="domestic_hot_water_setpoint",
|
| 393 |
translation_key="domestic_hot_water_setpoint",
|
| 394 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
| 395 |
device_class=SensorDeviceClass.TEMPERATURE,
|
| 396 |
entity_category=EntityCategory.DIAGNOSTIC,
|
| 397 |
+
state_class=SensorStateClass.MEASUREMENT,
|
| 398 |
),
|
| 399 |
)
|
| 400 |
|
| 401 |
|
| 402 |
async def async_setup_entry(
|
| 403 |
hass: HomeAssistant,
|
| 404 |
+
entry: PlugwiseConfigEntry,
|
| 405 |
+
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 406 |
) -> None:
|
| 407 |
"""Set up the Smile sensors from a config entry."""
|
| 408 |
+
coordinator = entry.runtime_data
|
| 409 |
|
| 410 |
+
@callback
|
| 411 |
+
def _add_entities() -> None:
|
| 412 |
+
"""Add Entities."""
|
| 413 |
+
if not coordinator.new_devices:
|
| 414 |
+
return
|
| 415 |
+
|
| 416 |
+
async_add_entities(
|
| 417 |
+
PlugwiseSensorEntity(coordinator, device_id, description)
|
| 418 |
+
for device_id in coordinator.new_devices
|
| 419 |
+
if (sensors := coordinator.data[device_id].get("sensors"))
|
| 420 |
+
for description in SENSORS
|
| 421 |
+
if description.key in sensors
|
| 422 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
|
| 424 |
+
_add_entities()
|
| 425 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 426 |
|
| 427 |
|
| 428 |
class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
|
|
|
|
| 438 |
) -> None:
|
| 439 |
"""Initialise the sensor."""
|
| 440 |
super().__init__(coordinator, device_id)
|
|
|
|
| 441 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 442 |
+
self.entity_description = description
|
| 443 |
|
| 444 |
@property
|
| 445 |
def native_value(self) -> int | float:
|
|
@@ -1,48 +1,47 @@
|
|
| 1 |
{
|
| 2 |
-
"options": {
|
| 3 |
-
"step": {
|
| 4 |
-
"none": {
|
| 5 |
-
"title": "No Options available",
|
| 6 |
-
"description": "This Integration does not provide any Options"
|
| 7 |
-
},
|
| 8 |
-
"init": {
|
| 9 |
-
"description": "Adjust Smile/Stretch Options",
|
| 10 |
-
"data": {
|
| 11 |
-
"cooling_on": "Anna: cooling-mode is on",
|
| 12 |
-
"scan_interval": "Scan Interval (seconds) *) beta-only option",
|
| 13 |
-
"homekit_emulation": "Homekit emulation (i.e. on hvac_off => Away) *) beta-only option",
|
| 14 |
-
"refresh_interval": "Frontend refresh-time (1.5 - 5 seconds) *) beta-only option"
|
| 15 |
-
}
|
| 16 |
-
}
|
| 17 |
-
}
|
| 18 |
-
},
|
| 19 |
"config": {
|
| 20 |
-
"
|
| 21 |
-
"
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
"password": "ID",
|
| 26 |
-
"username": "Username",
|
| 27 |
-
"host": "IP-address",
|
| 28 |
-
"port": "Port number"
|
| 29 |
-
}
|
| 30 |
-
}
|
| 31 |
},
|
| 32 |
"error": {
|
| 33 |
-
"cannot_connect": "
|
| 34 |
-
"invalid_auth": "
|
| 35 |
"invalid_setup": "Add your Adam instead of your Anna, see the documentation",
|
| 36 |
-
"network_down": "Plugwise Zigbee network is down",
|
| 37 |
-
"network_timeout": "Network communication timeout",
|
| 38 |
"response_error": "Invalid XML data, or error indication received",
|
| 39 |
-
"
|
| 40 |
-
"unknown": "Unknown error!",
|
| 41 |
"unsupported": "Device with unsupported firmware"
|
| 42 |
},
|
| 43 |
-
"
|
| 44 |
-
"
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
}
|
| 47 |
},
|
| 48 |
"entity": {
|
|
@@ -53,6 +52,9 @@
|
|
| 53 |
"cooling_enabled": {
|
| 54 |
"name": "Cooling enabled"
|
| 55 |
},
|
|
|
|
|
|
|
|
|
|
| 56 |
"dhw_state": {
|
| 57 |
"name": "DHW state"
|
| 58 |
},
|
|
@@ -60,230 +62,271 @@
|
|
| 60 |
"name": "Flame state"
|
| 61 |
},
|
| 62 |
"heating_state": {
|
| 63 |
-
"name": "
|
| 64 |
-
},
|
| 65 |
-
"cooling_state": {
|
| 66 |
-
"name": "Cooling"
|
| 67 |
-
},
|
| 68 |
-
"slave_boiler_state": {
|
| 69 |
-
"name": "Secondary boiler state"
|
| 70 |
},
|
| 71 |
"plugwise_notification": {
|
| 72 |
"name": "Plugwise notification"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
},
|
| 75 |
"climate": {
|
| 76 |
"plugwise": {
|
| 77 |
"state_attributes": {
|
|
|
|
|
|
|
|
|
|
| 78 |
"preset_mode": {
|
| 79 |
"state": {
|
| 80 |
"asleep": "Night",
|
| 81 |
-
"away": "
|
| 82 |
-
"home": "
|
| 83 |
"no_frost": "Anti-frost",
|
| 84 |
"vacation": "Vacation"
|
| 85 |
}
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
}
|
| 88 |
}
|
| 89 |
},
|
| 90 |
"number": {
|
| 91 |
-
"maximum_boiler_temperature": {
|
| 92 |
-
"name": "Maximum boiler temperature setpoint"
|
| 93 |
-
},
|
| 94 |
"max_dhw_temperature": {
|
| 95 |
"name": "Domestic hot water setpoint"
|
| 96 |
},
|
|
|
|
|
|
|
|
|
|
| 97 |
"temperature_offset": {
|
| 98 |
"name": "Temperature offset"
|
| 99 |
}
|
| 100 |
},
|
| 101 |
"select": {
|
| 102 |
-
"
|
| 103 |
"name": "DHW mode",
|
| 104 |
"state": {
|
| 105 |
-
"auto": "
|
| 106 |
-
"boost": "
|
| 107 |
-
"comfort": "
|
| 108 |
-
"off": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
}
|
| 110 |
},
|
| 111 |
-
"
|
| 112 |
"name": "Regulation mode",
|
| 113 |
"state": {
|
| 114 |
"bleeding_cold": "Bleeding cold",
|
| 115 |
"bleeding_hot": "Bleeding hot",
|
| 116 |
-
"cooling": "
|
| 117 |
-
"heating": "
|
| 118 |
-
"off": "
|
| 119 |
}
|
| 120 |
},
|
| 121 |
-
"
|
| 122 |
-
"name": "Thermostat schedule"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
},
|
| 125 |
"sensor": {
|
| 126 |
-
"setpoint": {
|
| 127 |
-
"name": "Setpoint"
|
| 128 |
-
},
|
| 129 |
"cooling_setpoint": {
|
| 130 |
"name": "Cooling setpoint"
|
| 131 |
},
|
| 132 |
-
"
|
| 133 |
-
"name": "
|
| 134 |
-
},
|
| 135 |
-
"intended_boiler_temperature": {
|
| 136 |
-
"name": "Intended boiler temperature"
|
| 137 |
-
},
|
| 138 |
-
"temperature_difference": {
|
| 139 |
-
"name": "Temperature difference"
|
| 140 |
-
},
|
| 141 |
-
"uncorrected_temperature": {
|
| 142 |
-
"name": "Uncorrected temperature"
|
| 143 |
-
},
|
| 144 |
-
"outdoor_temperature": {
|
| 145 |
-
"name": "Outdoor temperature"
|
| 146 |
-
},
|
| 147 |
-
"outdoor_air_temperature": {
|
| 148 |
-
"name": "Outdoor air temperature"
|
| 149 |
-
},
|
| 150 |
-
"water_temperature": {
|
| 151 |
-
"name": "Water temperature"
|
| 152 |
},
|
| 153 |
-
"
|
| 154 |
-
"name": "
|
| 155 |
},
|
| 156 |
"electricity_consumed": {
|
| 157 |
"name": "Electricity consumed"
|
| 158 |
},
|
| 159 |
-
"electricity_produced": {
|
| 160 |
-
"name": "Electricity produced"
|
| 161 |
-
},
|
| 162 |
-
"electricity_consumed_point": {
|
| 163 |
-
"name": "Electricity consumed point"
|
| 164 |
-
},
|
| 165 |
-
"electricity_produced_point": {
|
| 166 |
-
"name": "Electricity produced point"
|
| 167 |
-
},
|
| 168 |
"electricity_consumed_interval": {
|
| 169 |
"name": "Electricity consumed interval"
|
| 170 |
},
|
| 171 |
-
"
|
| 172 |
-
"name": "Electricity consumed peak
|
| 173 |
},
|
| 174 |
"electricity_consumed_off_peak_interval": {
|
| 175 |
-
"name": "Electricity consumed off
|
| 176 |
-
},
|
| 177 |
-
"electricity_produced_interval": {
|
| 178 |
-
"name": "Electricity produced interval"
|
| 179 |
-
},
|
| 180 |
-
"electricity_produced_peak_interval": {
|
| 181 |
-
"name": "Electricity produced peak interval"
|
| 182 |
-
},
|
| 183 |
-
"electricity_produced_off_peak_interval": {
|
| 184 |
-
"name": "Electricity produced off peak interval"
|
| 185 |
},
|
| 186 |
"electricity_consumed_off_peak_point": {
|
| 187 |
-
"name": "Electricity consumed off
|
| 188 |
-
},
|
| 189 |
-
"electricity_consumed_peak_point": {
|
| 190 |
-
"name": "Electricity consumed peak point"
|
| 191 |
-
},
|
| 192 |
-
"electricity_consumed_off_peak_cumulative": {
|
| 193 |
-
"name": "Electricity consumed off peak cumulative"
|
| 194 |
},
|
| 195 |
"electricity_consumed_peak_cumulative": {
|
| 196 |
"name": "Electricity consumed peak cumulative"
|
| 197 |
},
|
| 198 |
-
"
|
| 199 |
-
"name": "Electricity
|
| 200 |
-
},
|
| 201 |
-
"electricity_produced_peak_point": {
|
| 202 |
-
"name": "Electricity produced peak point"
|
| 203 |
},
|
| 204 |
-
"
|
| 205 |
-
"name": "Electricity
|
| 206 |
},
|
| 207 |
-
"
|
| 208 |
-
"name": "Electricity
|
| 209 |
},
|
| 210 |
"electricity_phase_one_consumed": {
|
| 211 |
"name": "Electricity phase one consumed"
|
| 212 |
},
|
| 213 |
-
"
|
| 214 |
-
"name": "Electricity phase
|
| 215 |
},
|
| 216 |
"electricity_phase_three_consumed": {
|
| 217 |
"name": "Electricity phase three consumed"
|
| 218 |
},
|
| 219 |
-
"
|
| 220 |
-
"name": "Electricity phase
|
|
|
|
|
|
|
|
|
|
| 221 |
},
|
| 222 |
"electricity_phase_two_produced": {
|
| 223 |
"name": "Electricity phase two produced"
|
| 224 |
},
|
| 225 |
-
"
|
| 226 |
-
"name": "Electricity
|
| 227 |
},
|
| 228 |
-
"
|
| 229 |
-
"name": "
|
| 230 |
},
|
| 231 |
-
"
|
| 232 |
-
"name": "
|
| 233 |
},
|
| 234 |
-
"
|
| 235 |
-
"name": "
|
| 236 |
},
|
| 237 |
-
"
|
| 238 |
-
"name": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
},
|
| 240 |
"gas_consumed_cumulative": {
|
| 241 |
"name": "Gas consumed cumulative"
|
| 242 |
},
|
| 243 |
-
"
|
| 244 |
-
"name": "
|
| 245 |
},
|
| 246 |
-
"
|
| 247 |
-
"name": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
},
|
| 249 |
"modulation_level": {
|
| 250 |
"name": "Modulation level"
|
| 251 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
"valve_position": {
|
| 253 |
"name": "Valve position"
|
| 254 |
},
|
| 255 |
-
"
|
| 256 |
-
"name": "
|
| 257 |
},
|
| 258 |
-
"
|
| 259 |
-
"name": "
|
| 260 |
},
|
| 261 |
-
"
|
| 262 |
-
"name": "
|
| 263 |
},
|
| 264 |
-
"
|
| 265 |
-
"name": "
|
|
|
|
|
|
|
|
|
|
| 266 |
}
|
| 267 |
},
|
| 268 |
"switch": {
|
| 269 |
"cooling_ena_switch": {
|
| 270 |
-
"name": "
|
| 271 |
},
|
| 272 |
"dhw_cm_switch": {
|
| 273 |
"name": "DHW comfort mode"
|
| 274 |
},
|
| 275 |
"lock": {
|
| 276 |
-
"name": "
|
| 277 |
},
|
| 278 |
"relay": {
|
| 279 |
"name": "Relay"
|
| 280 |
}
|
| 281 |
}
|
| 282 |
},
|
| 283 |
-
"
|
| 284 |
-
"
|
| 285 |
-
"
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
}
|
| 288 |
}
|
| 289 |
}
|
|
|
|
| 1 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
"config": {
|
| 3 |
+
"abort": {
|
| 4 |
+
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
| 5 |
+
"anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
|
| 6 |
+
"not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address.",
|
| 7 |
+
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
},
|
| 9 |
"error": {
|
| 10 |
+
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
| 11 |
+
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
| 12 |
"invalid_setup": "Add your Adam instead of your Anna, see the documentation",
|
|
|
|
|
|
|
| 13 |
"response_error": "Invalid XML data, or error indication received",
|
| 14 |
+
"unknown": "[%key:common::config_flow::error::unknown%]",
|
|
|
|
| 15 |
"unsupported": "Device with unsupported firmware"
|
| 16 |
},
|
| 17 |
+
"step": {
|
| 18 |
+
"reconfigure": {
|
| 19 |
+
"data": {
|
| 20 |
+
"host": "[%key:common::config_flow::data::ip%]",
|
| 21 |
+
"port": "[%key:common::config_flow::data::port%]"
|
| 22 |
+
},
|
| 23 |
+
"data_description": {
|
| 24 |
+
"host": "[%key:component::plugwise::config::step::user::data_description::host%]",
|
| 25 |
+
"port": "[%key:component::plugwise::config::step::user::data_description::port%]"
|
| 26 |
+
},
|
| 27 |
+
"description": "Update configuration for {title}."
|
| 28 |
+
},
|
| 29 |
+
"user": {
|
| 30 |
+
"data": {
|
| 31 |
+
"host": "[%key:common::config_flow::data::ip%]",
|
| 32 |
+
"password": "Smile ID",
|
| 33 |
+
"port": "[%key:common::config_flow::data::port%]",
|
| 34 |
+
"username": "Smile username"
|
| 35 |
+
},
|
| 36 |
+
"data_description": {
|
| 37 |
+
"host": "The hostname or IP address of your Smile. You can find it in your router or the Plugwise app.",
|
| 38 |
+
"password": "The Smile ID printed on the label on the back of your Adam, Smile-T, or P1.",
|
| 39 |
+
"port": "By default your Smile uses port 80, normally you should not have to change this.",
|
| 40 |
+
"username": "Default is `smile`, or `stretch` for the legacy Stretch."
|
| 41 |
+
},
|
| 42 |
+
"description": "Please enter",
|
| 43 |
+
"title": "Connect to the Smile"
|
| 44 |
+
}
|
| 45 |
}
|
| 46 |
},
|
| 47 |
"entity": {
|
|
|
|
| 52 |
"cooling_enabled": {
|
| 53 |
"name": "Cooling enabled"
|
| 54 |
},
|
| 55 |
+
"cooling_state": {
|
| 56 |
+
"name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
|
| 57 |
+
},
|
| 58 |
"dhw_state": {
|
| 59 |
"name": "DHW state"
|
| 60 |
},
|
|
|
|
| 62 |
"name": "Flame state"
|
| 63 |
},
|
| 64 |
"heating_state": {
|
| 65 |
+
"name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
},
|
| 67 |
"plugwise_notification": {
|
| 68 |
"name": "Plugwise notification"
|
| 69 |
+
},
|
| 70 |
+
"secondary_boiler_state": {
|
| 71 |
+
"name": "Secondary boiler state"
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"button": {
|
| 75 |
+
"reboot": {
|
| 76 |
+
"name": "Reboot"
|
| 77 |
}
|
| 78 |
},
|
| 79 |
"climate": {
|
| 80 |
"plugwise": {
|
| 81 |
"state_attributes": {
|
| 82 |
+
"available_schemas": {
|
| 83 |
+
"name": "Available schemas"
|
| 84 |
+
},
|
| 85 |
"preset_mode": {
|
| 86 |
"state": {
|
| 87 |
"asleep": "Night",
|
| 88 |
+
"away": "[%key:common::state::not_home%]",
|
| 89 |
+
"home": "[%key:common::state::home%]",
|
| 90 |
"no_frost": "Anti-frost",
|
| 91 |
"vacation": "Vacation"
|
| 92 |
}
|
| 93 |
+
},
|
| 94 |
+
"selected_schema": {
|
| 95 |
+
"name": "Selected schema"
|
| 96 |
}
|
| 97 |
}
|
| 98 |
}
|
| 99 |
},
|
| 100 |
"number": {
|
|
|
|
|
|
|
|
|
|
| 101 |
"max_dhw_temperature": {
|
| 102 |
"name": "Domestic hot water setpoint"
|
| 103 |
},
|
| 104 |
+
"maximum_boiler_temperature": {
|
| 105 |
+
"name": "Maximum boiler temperature setpoint"
|
| 106 |
+
},
|
| 107 |
"temperature_offset": {
|
| 108 |
"name": "Temperature offset"
|
| 109 |
}
|
| 110 |
},
|
| 111 |
"select": {
|
| 112 |
+
"select_dhw_mode": {
|
| 113 |
"name": "DHW mode",
|
| 114 |
"state": {
|
| 115 |
+
"auto": "[%key:common::state::auto%]",
|
| 116 |
+
"boost": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::boost%]",
|
| 117 |
+
"comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
|
| 118 |
+
"off": "[%key:common::state::off%]"
|
| 119 |
+
}
|
| 120 |
+
},
|
| 121 |
+
"select_gateway_mode": {
|
| 122 |
+
"name": "Gateway mode",
|
| 123 |
+
"state": {
|
| 124 |
+
"away": "Pause",
|
| 125 |
+
"full": "[%key:common::state::normal%]",
|
| 126 |
+
"vacation": "Vacation"
|
| 127 |
}
|
| 128 |
},
|
| 129 |
+
"select_regulation_mode": {
|
| 130 |
"name": "Regulation mode",
|
| 131 |
"state": {
|
| 132 |
"bleeding_cold": "Bleeding cold",
|
| 133 |
"bleeding_hot": "Bleeding hot",
|
| 134 |
+
"cooling": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]",
|
| 135 |
+
"heating": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]",
|
| 136 |
+
"off": "[%key:common::state::off%]"
|
| 137 |
}
|
| 138 |
},
|
| 139 |
+
"select_schedule": {
|
| 140 |
+
"name": "Thermostat schedule",
|
| 141 |
+
"state": {
|
| 142 |
+
"off": "[%key:common::state::off%]"
|
| 143 |
+
}
|
| 144 |
+
},
|
| 145 |
+
"select_zone_profile": {
|
| 146 |
+
"name": "Zone profile",
|
| 147 |
+
"state": {
|
| 148 |
+
"active": "[%key:common::state::active%]",
|
| 149 |
+
"off": "[%key:common::state::off%]",
|
| 150 |
+
"passive": "Passive"
|
| 151 |
+
}
|
| 152 |
}
|
| 153 |
},
|
| 154 |
"sensor": {
|
|
|
|
|
|
|
|
|
|
| 155 |
"cooling_setpoint": {
|
| 156 |
"name": "Cooling setpoint"
|
| 157 |
},
|
| 158 |
+
"dhw_temperature": {
|
| 159 |
+
"name": "DHW temperature"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
},
|
| 161 |
+
"domestic_hot_water_setpoint": {
|
| 162 |
+
"name": "DHW setpoint"
|
| 163 |
},
|
| 164 |
"electricity_consumed": {
|
| 165 |
"name": "Electricity consumed"
|
| 166 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
"electricity_consumed_interval": {
|
| 168 |
"name": "Electricity consumed interval"
|
| 169 |
},
|
| 170 |
+
"electricity_consumed_off_peak_cumulative": {
|
| 171 |
+
"name": "Electricity consumed off-peak cumulative"
|
| 172 |
},
|
| 173 |
"electricity_consumed_off_peak_interval": {
|
| 174 |
+
"name": "Electricity consumed off-peak interval"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
},
|
| 176 |
"electricity_consumed_off_peak_point": {
|
| 177 |
+
"name": "Electricity consumed off-peak point"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
},
|
| 179 |
"electricity_consumed_peak_cumulative": {
|
| 180 |
"name": "Electricity consumed peak cumulative"
|
| 181 |
},
|
| 182 |
+
"electricity_consumed_peak_interval": {
|
| 183 |
+
"name": "Electricity consumed peak interval"
|
|
|
|
|
|
|
|
|
|
| 184 |
},
|
| 185 |
+
"electricity_consumed_peak_point": {
|
| 186 |
+
"name": "Electricity consumed peak point"
|
| 187 |
},
|
| 188 |
+
"electricity_consumed_point": {
|
| 189 |
+
"name": "Electricity consumed point"
|
| 190 |
},
|
| 191 |
"electricity_phase_one_consumed": {
|
| 192 |
"name": "Electricity phase one consumed"
|
| 193 |
},
|
| 194 |
+
"electricity_phase_one_produced": {
|
| 195 |
+
"name": "Electricity phase one produced"
|
| 196 |
},
|
| 197 |
"electricity_phase_three_consumed": {
|
| 198 |
"name": "Electricity phase three consumed"
|
| 199 |
},
|
| 200 |
+
"electricity_phase_three_produced": {
|
| 201 |
+
"name": "Electricity phase three produced"
|
| 202 |
+
},
|
| 203 |
+
"electricity_phase_two_consumed": {
|
| 204 |
+
"name": "Electricity phase two consumed"
|
| 205 |
},
|
| 206 |
"electricity_phase_two_produced": {
|
| 207 |
"name": "Electricity phase two produced"
|
| 208 |
},
|
| 209 |
+
"electricity_produced": {
|
| 210 |
+
"name": "Electricity produced"
|
| 211 |
},
|
| 212 |
+
"electricity_produced_interval": {
|
| 213 |
+
"name": "Electricity produced interval"
|
| 214 |
},
|
| 215 |
+
"electricity_produced_off_peak_cumulative": {
|
| 216 |
+
"name": "Electricity produced off-peak cumulative"
|
| 217 |
},
|
| 218 |
+
"electricity_produced_off_peak_interval": {
|
| 219 |
+
"name": "Electricity produced off-peak interval"
|
| 220 |
},
|
| 221 |
+
"electricity_produced_off_peak_point": {
|
| 222 |
+
"name": "Electricity produced off-peak point"
|
| 223 |
+
},
|
| 224 |
+
"electricity_produced_peak_cumulative": {
|
| 225 |
+
"name": "Electricity produced peak cumulative"
|
| 226 |
+
},
|
| 227 |
+
"electricity_produced_peak_interval": {
|
| 228 |
+
"name": "Electricity produced peak interval"
|
| 229 |
+
},
|
| 230 |
+
"electricity_produced_peak_point": {
|
| 231 |
+
"name": "Electricity produced peak point"
|
| 232 |
+
},
|
| 233 |
+
"electricity_produced_point": {
|
| 234 |
+
"name": "Electricity produced point"
|
| 235 |
},
|
| 236 |
"gas_consumed_cumulative": {
|
| 237 |
"name": "Gas consumed cumulative"
|
| 238 |
},
|
| 239 |
+
"gas_consumed_interval": {
|
| 240 |
+
"name": "Gas consumed interval"
|
| 241 |
},
|
| 242 |
+
"heating_setpoint": {
|
| 243 |
+
"name": "Heating setpoint"
|
| 244 |
+
},
|
| 245 |
+
"intended_boiler_temperature": {
|
| 246 |
+
"name": "Intended boiler temperature"
|
| 247 |
+
},
|
| 248 |
+
"maximum_boiler_temperature": {
|
| 249 |
+
"name": "Maximum boiler temperature"
|
| 250 |
},
|
| 251 |
"modulation_level": {
|
| 252 |
"name": "Modulation level"
|
| 253 |
},
|
| 254 |
+
"net_electricity_cumulative": {
|
| 255 |
+
"name": "Net electricity cumulative"
|
| 256 |
+
},
|
| 257 |
+
"net_electricity_point": {
|
| 258 |
+
"name": "Net electricity point"
|
| 259 |
+
},
|
| 260 |
+
"outdoor_air_temperature": {
|
| 261 |
+
"name": "Outdoor air temperature"
|
| 262 |
+
},
|
| 263 |
+
"outdoor_temperature": {
|
| 264 |
+
"name": "Outdoor temperature"
|
| 265 |
+
},
|
| 266 |
+
"return_temperature": {
|
| 267 |
+
"name": "Return temperature"
|
| 268 |
+
},
|
| 269 |
+
"setpoint": {
|
| 270 |
+
"name": "Setpoint"
|
| 271 |
+
},
|
| 272 |
+
"temperature_difference": {
|
| 273 |
+
"name": "Temperature difference"
|
| 274 |
+
},
|
| 275 |
"valve_position": {
|
| 276 |
"name": "Valve position"
|
| 277 |
},
|
| 278 |
+
"voltage_phase_one": {
|
| 279 |
+
"name": "Voltage phase one"
|
| 280 |
},
|
| 281 |
+
"voltage_phase_three": {
|
| 282 |
+
"name": "Voltage phase three"
|
| 283 |
},
|
| 284 |
+
"voltage_phase_two": {
|
| 285 |
+
"name": "Voltage phase two"
|
| 286 |
},
|
| 287 |
+
"water_pressure": {
|
| 288 |
+
"name": "Water pressure"
|
| 289 |
+
},
|
| 290 |
+
"water_temperature": {
|
| 291 |
+
"name": "Water temperature"
|
| 292 |
}
|
| 293 |
},
|
| 294 |
"switch": {
|
| 295 |
"cooling_ena_switch": {
|
| 296 |
+
"name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
|
| 297 |
},
|
| 298 |
"dhw_cm_switch": {
|
| 299 |
"name": "DHW comfort mode"
|
| 300 |
},
|
| 301 |
"lock": {
|
| 302 |
+
"name": "[%key:component::lock::title%]"
|
| 303 |
},
|
| 304 |
"relay": {
|
| 305 |
"name": "Relay"
|
| 306 |
}
|
| 307 |
}
|
| 308 |
},
|
| 309 |
+
"exceptions": {
|
| 310 |
+
"authentication_failed": {
|
| 311 |
+
"message": "[%key:common::config_flow::error::invalid_auth%]"
|
| 312 |
+
},
|
| 313 |
+
"data_incomplete_or_missing": {
|
| 314 |
+
"message": "Data incomplete or missing."
|
| 315 |
+
},
|
| 316 |
+
"error_communicating_with_api": {
|
| 317 |
+
"message": "Error communicating with API: {error}."
|
| 318 |
+
},
|
| 319 |
+
"failed_to_connect": {
|
| 320 |
+
"message": "[%key:common::config_flow::error::cannot_connect%]"
|
| 321 |
+
},
|
| 322 |
+
"invalid_xml_data": {
|
| 323 |
+
"message": "[%key:component::plugwise::config::error::response_error%]"
|
| 324 |
+
},
|
| 325 |
+
"set_schedule_first": {
|
| 326 |
+
"message": "Failed setting HVACMode, set a schedule first."
|
| 327 |
+
},
|
| 328 |
+
"unsupported_firmware": {
|
| 329 |
+
"message": "[%key:component::plugwise::config::error::unsupported%]"
|
| 330 |
}
|
| 331 |
}
|
| 332 |
}
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
"""Plugwise Switch component for HomeAssistant."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
from dataclasses import dataclass
|
|
@@ -11,22 +12,18 @@
|
|
| 11 |
SwitchEntity,
|
| 12 |
SwitchEntityDescription,
|
| 13 |
)
|
| 14 |
-
from homeassistant.config_entries import ConfigEntry
|
| 15 |
from homeassistant.const import EntityCategory
|
| 16 |
-
from homeassistant.core import HomeAssistant
|
| 17 |
-
from homeassistant.helpers.entity_platform import
|
| 18 |
|
| 19 |
-
from .
|
| 20 |
-
COORDINATOR, # pw-beta
|
| 21 |
-
DOMAIN,
|
| 22 |
-
LOGGER,
|
| 23 |
-
)
|
| 24 |
-
from .coordinator import PlugwiseDataUpdateCoordinator
|
| 25 |
from .entity import PlugwiseEntity
|
| 26 |
from .util import plugwise_command
|
| 27 |
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
@dataclass
|
| 30 |
class PlugwiseSwitchEntityDescription(SwitchEntityDescription):
|
| 31 |
"""Describes Plugwise switch entity."""
|
| 32 |
|
|
@@ -37,15 +34,11 @@
|
|
| 37 |
PlugwiseSwitchEntityDescription(
|
| 38 |
key="dhw_cm_switch",
|
| 39 |
translation_key="dhw_cm_switch",
|
| 40 |
-
icon="mdi:water-plus",
|
| 41 |
-
device_class=SwitchDeviceClass.SWITCH,
|
| 42 |
entity_category=EntityCategory.CONFIG,
|
| 43 |
),
|
| 44 |
PlugwiseSwitchEntityDescription(
|
| 45 |
key="lock",
|
| 46 |
translation_key="lock",
|
| 47 |
-
icon="mdi:lock",
|
| 48 |
-
device_class=SwitchDeviceClass.SWITCH,
|
| 49 |
entity_category=EntityCategory.CONFIG,
|
| 50 |
),
|
| 51 |
PlugwiseSwitchEntityDescription(
|
|
@@ -56,8 +49,6 @@
|
|
| 56 |
PlugwiseSwitchEntityDescription(
|
| 57 |
key="cooling_ena_switch",
|
| 58 |
translation_key="cooling_ena_switch",
|
| 59 |
-
icon="mdi:snowflake-thermometer",
|
| 60 |
-
device_class=SwitchDeviceClass.SWITCH,
|
| 61 |
entity_category=EntityCategory.CONFIG,
|
| 62 |
),
|
| 63 |
)
|
|
@@ -65,24 +56,28 @@
|
|
| 65 |
|
| 66 |
async def async_setup_entry(
|
| 67 |
hass: HomeAssistant,
|
| 68 |
-
|
| 69 |
-
async_add_entities:
|
| 70 |
) -> None:
|
| 71 |
"""Set up the Smile switches from a config entry."""
|
| 72 |
-
coordinator =
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
)
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
|
|
@@ -98,8 +93,8 @@
|
|
| 98 |
) -> None:
|
| 99 |
"""Set up the Plugwise API."""
|
| 100 |
super().__init__(coordinator, device_id)
|
| 101 |
-
self.entity_description = description
|
| 102 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
|
|
|
| 103 |
|
| 104 |
@property
|
| 105 |
def is_on(self) -> bool:
|
|
|
|
| 1 |
"""Plugwise Switch component for HomeAssistant."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from dataclasses import dataclass
|
|
|
|
| 12 |
SwitchEntity,
|
| 13 |
SwitchEntityDescription,
|
| 14 |
)
|
|
|
|
| 15 |
from homeassistant.const import EntityCategory
|
| 16 |
+
from homeassistant.core import HomeAssistant, callback
|
| 17 |
+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
| 18 |
|
| 19 |
+
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
from .entity import PlugwiseEntity
|
| 21 |
from .util import plugwise_command
|
| 22 |
|
| 23 |
+
PARALLEL_UPDATES = 0
|
| 24 |
+
|
| 25 |
|
| 26 |
+
@dataclass(frozen=True)
|
| 27 |
class PlugwiseSwitchEntityDescription(SwitchEntityDescription):
|
| 28 |
"""Describes Plugwise switch entity."""
|
| 29 |
|
|
|
|
| 34 |
PlugwiseSwitchEntityDescription(
|
| 35 |
key="dhw_cm_switch",
|
| 36 |
translation_key="dhw_cm_switch",
|
|
|
|
|
|
|
| 37 |
entity_category=EntityCategory.CONFIG,
|
| 38 |
),
|
| 39 |
PlugwiseSwitchEntityDescription(
|
| 40 |
key="lock",
|
| 41 |
translation_key="lock",
|
|
|
|
|
|
|
| 42 |
entity_category=EntityCategory.CONFIG,
|
| 43 |
),
|
| 44 |
PlugwiseSwitchEntityDescription(
|
|
|
|
| 49 |
PlugwiseSwitchEntityDescription(
|
| 50 |
key="cooling_ena_switch",
|
| 51 |
translation_key="cooling_ena_switch",
|
|
|
|
|
|
|
| 52 |
entity_category=EntityCategory.CONFIG,
|
| 53 |
),
|
| 54 |
)
|
|
|
|
| 56 |
|
| 57 |
async def async_setup_entry(
|
| 58 |
hass: HomeAssistant,
|
| 59 |
+
entry: PlugwiseConfigEntry,
|
| 60 |
+
async_add_entities: AddConfigEntryEntitiesCallback,
|
| 61 |
) -> None:
|
| 62 |
"""Set up the Smile switches from a config entry."""
|
| 63 |
+
coordinator = entry.runtime_data
|
| 64 |
+
|
| 65 |
+
@callback
|
| 66 |
+
def _add_entities() -> None:
|
| 67 |
+
"""Add Entities."""
|
| 68 |
+
if not coordinator.new_devices:
|
| 69 |
+
return
|
| 70 |
+
|
| 71 |
+
async_add_entities(
|
| 72 |
+
PlugwiseSwitchEntity(coordinator, device_id, description)
|
| 73 |
+
for device_id in coordinator.new_devices
|
| 74 |
+
if (switches := coordinator.data[device_id].get("switches"))
|
| 75 |
+
for description in SWITCHES
|
| 76 |
+
if description.key in switches
|
| 77 |
+
)
|
| 78 |
|
| 79 |
+
_add_entities()
|
| 80 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
| 81 |
|
| 82 |
|
| 83 |
class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
|
|
|
|
| 93 |
) -> None:
|
| 94 |
"""Set up the Plugwise API."""
|
| 95 |
super().__init__(coordinator, device_id)
|
|
|
|
| 96 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
| 97 |
+
self.entity_description = description
|
| 98 |
|
| 99 |
@property
|
| 100 |
def is_on(self) -> bool:
|
|
@@ -1,20 +1,18 @@
|
|
| 1 |
"""Utilities for Plugwise."""
|
|
|
|
| 2 |
from collections.abc import Awaitable, Callable, Coroutine
|
| 3 |
-
from typing import Any, Concatenate
|
| 4 |
|
| 5 |
from plugwise.exceptions import PlugwiseException
|
| 6 |
|
| 7 |
from homeassistant.exceptions import HomeAssistantError
|
| 8 |
|
|
|
|
| 9 |
from .entity import PlugwiseEntity
|
| 10 |
|
| 11 |
-
_PlugwiseEntityT = TypeVar("_PlugwiseEntityT", bound=PlugwiseEntity)
|
| 12 |
-
_R = TypeVar("_R")
|
| 13 |
-
_P = ParamSpec("_P")
|
| 14 |
-
|
| 15 |
|
| 16 |
-
def plugwise_command(
|
| 17 |
-
func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]]
|
| 18 |
) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
|
| 19 |
"""Decorate Plugwise calls that send commands/make changes to the device.
|
| 20 |
|
|
@@ -27,10 +25,14 @@
|
|
| 27 |
) -> _R:
|
| 28 |
try:
|
| 29 |
return await func(self, *args, **kwargs)
|
| 30 |
-
except PlugwiseException as
|
| 31 |
raise HomeAssistantError(
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
finally:
|
| 35 |
await self.coordinator.async_request_refresh()
|
| 36 |
|
|
|
|
| 1 |
"""Utilities for Plugwise."""
|
| 2 |
+
|
| 3 |
from collections.abc import Awaitable, Callable, Coroutine
|
| 4 |
+
from typing import Any, Concatenate
|
| 5 |
|
| 6 |
from plugwise.exceptions import PlugwiseException
|
| 7 |
|
| 8 |
from homeassistant.exceptions import HomeAssistantError
|
| 9 |
|
| 10 |
+
from .const import DOMAIN
|
| 11 |
from .entity import PlugwiseEntity
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
|
| 15 |
+
func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],
|
| 16 |
) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
|
| 17 |
"""Decorate Plugwise calls that send commands/make changes to the device.
|
| 18 |
|
|
|
|
| 25 |
) -> _R:
|
| 26 |
try:
|
| 27 |
return await func(self, *args, **kwargs)
|
| 28 |
+
except PlugwiseException as err:
|
| 29 |
raise HomeAssistantError(
|
| 30 |
+
translation_domain=DOMAIN,
|
| 31 |
+
translation_key="error_communicating_with_api",
|
| 32 |
+
translation_placeholders={
|
| 33 |
+
"error": str(err),
|
| 34 |
+
},
|
| 35 |
+
) from err
|
| 36 |
finally:
|
| 37 |
await self.coordinator.async_request_refresh()
|
| 38 |
|