Diff to HTML by rtfpessoa

Files changed (15) hide show
  1. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/__init__.py +41 -81
  2. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/binary_sensor.py +100 -67
  3. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py +222 -109
  4. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py +141 -225
  5. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py +51 -183
  6. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py +164 -63
  7. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/diagnostics.py +4 -13
  8. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py +36 -31
  9. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json +6 -6
  10. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py +55 -88
  11. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py +74 -72
  12. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py +415 -32
  13. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/strings.json +297 -65
  14. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py +74 -27
  15. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py +13 -12
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/__init__.py RENAMED
@@ -1,114 +1,68 @@
1
  """Plugwise platform for Home Assistant Core."""
2
- from __future__ import annotations
3
 
4
  from typing import Any
5
 
6
- import voluptuous as vol
7
-
8
- from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
9
- from homeassistant.config_entries import ConfigEntry
10
  from homeassistant.const import Platform
11
  from homeassistant.core import HomeAssistant, callback
12
  from homeassistant.helpers import device_registry as dr, entity_registry as er
13
- from plugwise.exceptions import PlugwiseError
14
 
15
- from .const import (
16
- COORDINATOR,
17
- DOMAIN,
18
- LOGGER,
19
- PLATFORMS_GATEWAY,
20
- SERVICE_DELETE,
21
- UNDO_UPDATE_LISTENER,
22
- )
23
- from .const import CONF_REFRESH_INTERVAL # pw-beta options
24
- from .coordinator import PlugwiseDataUpdateCoordinator
25
 
26
 
27
- async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
28
- """Set up Plugwise Smiles from a config entry."""
29
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
30
 
31
- cooldown = 1.5 # pw-beta frontend refresh-interval
32
- if (
33
- custom_refresh := entry.options.get(CONF_REFRESH_INTERVAL)
34
- ) is not None: # pragma: no cover
35
- cooldown = custom_refresh
36
- LOGGER.debug("DUC cooldown interval: %s", cooldown)
37
-
38
- coordinator = PlugwiseDataUpdateCoordinator(
39
- hass, entry, cooldown
40
- ) # pw-beta - cooldown, update_interval as extra
41
  await coordinator.async_config_entry_first_refresh()
42
- # Migrate a changed sensor unique_id
43
  migrate_sensor_entities(hass, coordinator)
44
 
45
- undo_listener = entry.add_update_listener(_update_listener) # pw-beta
46
-
47
- hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
48
- COORDINATOR: coordinator, # pw-beta
49
- UNDO_UPDATE_LISTENER: undo_listener, # pw-beta
50
- }
51
 
52
  device_registry = dr.async_get(hass)
53
  device_registry.async_get_or_create(
54
  config_entry_id=entry.entry_id,
55
  identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
56
  manufacturer="Plugwise",
57
- model=coordinator.api.smile_model,
58
- name=coordinator.api.smile_name,
59
- sw_version=coordinator.api.smile_version[0],
60
- )
61
-
62
- async def delete_notification(
63
- self,
64
- ): # pragma: no cover # pw-beta: HA service - delete_notification
65
- """Service: delete the Plugwise Notification."""
66
- LOGGER.debug(
67
- "Service delete PW Notification called for %s", coordinator.api.smile_name
68
- )
69
- try:
70
- deleted = await coordinator.api.delete_notification()
71
- LOGGER.debug("PW Notification deleted: %s", 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_GATEWAY)
79
-
80
- for component in PLATFORMS_GATEWAY: # 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 _update_listener(
90
- hass: HomeAssistant, entry: ConfigEntry
91
- ): # pragma: no cover # pw-beta
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):
97
- """Unload a config entry."""
98
- if unload_ok := await hass.config_entries.async_unload_platforms(
99
- entry, PLATFORMS_GATEWAY
100
- ):
101
- hass.data[DOMAIN].pop(entry.entry_id)
102
- return unload_ok
103
 
104
 
105
  @callback
106
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
107
  """Migrate Plugwise entity entries.
108
 
109
- - Migrates unique ID from old relay switches to the new unique ID
 
110
  """
111
- if entry.domain == SWITCH_DOMAIN and entry.unique_id.endswith("-plug"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")}
113
 
114
  # No migration needed
@@ -122,10 +76,10 @@
122
  """Migrate Sensors if needed."""
123
  ent_reg = er.async_get(hass)
124
 
125
- # Migrate opentherm_outdoor_temperature # pw-beta add to Core
126
  # to opentherm_outdoor_air_temperature sensor
127
- for device_id, device in coordinator.data.devices.items():
128
- if device["dev_class"] != "heater_central": # pw-beta add to Core
129
  continue
130
 
131
  old_unique_id = f"{device_id}-outdoor_temperature"
@@ -133,4 +87,10 @@
133
  Platform.SENSOR, DOMAIN, old_unique_id
134
  ):
135
  new_unique_id = f"{device_id}-outdoor_air_temperature"
 
 
 
 
 
 
136
  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
 
1
  """Plugwise platform for Home Assistant Core."""
 
2
 
3
  from typing import Any
4
 
 
 
 
 
5
  from homeassistant.const import Platform
6
  from homeassistant.core import HomeAssistant, callback
7
  from homeassistant.helpers import device_registry as dr, entity_registry as er
 
8
 
9
+ from .const import DEV_CLASS, DOMAIN, LOGGER, PLATFORMS
10
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
 
 
 
11
 
12
 
13
+ async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
14
+ """Set up Plugwise components from a config entry."""
15
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
16
 
17
+ coordinator = PlugwiseDataUpdateCoordinator(hass, entry)
 
 
 
 
 
 
 
 
 
18
  await coordinator.async_config_entry_first_refresh()
 
19
  migrate_sensor_entities(hass, coordinator)
20
 
21
+ entry.runtime_data = coordinator
 
 
 
 
 
22
 
23
  device_registry = dr.async_get(hass)
24
  device_registry.async_get_or_create(
25
  config_entry_id=entry.entry_id,
26
  identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
27
  manufacturer="Plugwise",
28
+ model=coordinator.api.smile.model,
29
+ model_id=coordinator.api.smile.model_id,
30
+ name=coordinator.api.smile.name,
31
+ sw_version=str(coordinator.api.smile.version),
32
+ ) # required for adding the entity-less P1 Gateway
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
 
 
 
 
 
 
35
 
36
  return True
37
 
38
 
39
+ async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
40
+ """Unload the Plugwise components."""
41
+ return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
44
  @callback
45
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
46
  """Migrate Plugwise entity entries.
47
 
48
+ Migrates old unique ID's from old binary_sensors and
49
+ switches to the new unique ID's.
50
  """
51
+ if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
52
+ "-slave_boiler_state"
53
+ ):
54
+ return {
55
+ "new_unique_id": entry.unique_id.replace(
56
+ "-slave_boiler_state", "-secondary_boiler_state"
57
+ )
58
+ }
59
+ if entry.domain == Platform.SENSOR and entry.unique_id.endswith(
60
+ "-relative_humidity"
61
+ ):
62
+ return {
63
+ "new_unique_id": entry.unique_id.replace("-relative_humidity", "-humidity")
64
+ }
65
+ if entry.domain == Platform.SWITCH and entry.unique_id.endswith("-plug"):
66
  return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")}
67
 
68
  # No migration needed
 
76
  """Migrate Sensors if needed."""
77
  ent_reg = er.async_get(hass)
78
 
79
+ # Migrating opentherm_outdoor_temperature
80
  # to opentherm_outdoor_air_temperature sensor
81
+ for device_id, device in coordinator.data.items():
82
+ if device[DEV_CLASS] != "heater_central":
83
  continue
84
 
85
  old_unique_id = f"{device_id}-outdoor_temperature"
 
87
  Platform.SENSOR, DOMAIN, old_unique_id
88
  ):
89
  new_unique_id = f"{device_id}-outdoor_air_temperature"
90
+ LOGGER.debug(
91
+ "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
92
+ entity_id,
93
+ old_unique_id,
94
+ new_unique_id,
95
+ )
96
  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/binary_sensor.py RENAMED
@@ -1,51 +1,109 @@
1
  """Plugwise Binary Sensor component for Home Assistant."""
2
- from __future__ import annotations
3
 
4
  from collections.abc import Mapping
5
- from typing import Any
 
6
 
7
- from homeassistant.components.binary_sensor import BinarySensorEntity
8
- from homeassistant.config_entries import ConfigEntry
9
- from homeassistant.core import HomeAssistant
10
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
11
 
12
- from .const import COORDINATOR, DOMAIN, LOGGER, SEVERITIES # pw-beta
 
 
 
 
 
 
 
13
 
14
- from .coordinator import PlugwiseDataUpdateCoordinator
15
  from .entity import PlugwiseEntity
16
- from .models import PW_BINARY_SENSOR_TYPES, PlugwiseBinarySensorEntityDescription
17
 
 
 
 
18
  PARALLEL_UPDATES = 0
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  async def async_setup_entry(
22
  hass: HomeAssistant,
23
- config_entry: ConfigEntry,
24
- async_add_entities: AddEntitiesCallback,
25
  ) -> None:
26
  """Set up the Smile binary_sensors from a config entry."""
27
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][
28
- config_entry.entry_id
29
- ][COORDINATOR]
30
-
31
- entities: list[PlugwiseBinarySensorEntity] = []
32
- for device_id, device in coordinator.data.devices.items():
33
- for description in PW_BINARY_SENSOR_TYPES:
34
- if (
35
- "binary_sensors" not in device
36
- or description.key not in device["binary_sensors"]
37
- ):
38
- continue
39
-
40
- entities.append(
41
- PlugwiseBinarySensorEntity(
42
- coordinator,
43
- device_id,
44
- description,
45
- )
46
- )
47
- LOGGER.debug("Add %s binary sensor", description.key)
48
- async_add_entities(entities)
49
 
50
 
51
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
@@ -63,53 +121,28 @@
63
  super().__init__(coordinator, device_id)
64
  self.entity_description = description
65
  self._attr_unique_id = f"{device_id}-{description.key}"
66
- self._notification: dict[str, str] = {} # pw-beta
67
 
68
  @property
69
- def is_on(self) -> bool | None:
 
70
  """Return true if the binary sensor is on."""
71
- if (
72
- self._notification
73
- ): # pw-beta: show Plugwise notifications as HA persistent notifications
74
- for notify_id, message in self._notification.items():
75
- self.hass.components.persistent_notification.async_create(
76
- message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
77
- )
78
-
79
- return self.device["binary_sensors"].get(self.entity_description.key)
80
-
81
- @property
82
- def icon(self) -> str | None:
83
- """Return the icon to use in the frontend, if any."""
84
- if (icon_off := self.entity_description.icon_off) and self.is_on is False:
85
- return icon_off
86
- return self.entity_description.icon
87
 
88
  @property
 
89
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
90
  """Return entity specific state attributes."""
91
  if self.entity_description.key != "plugwise_notification":
92
  return None
93
 
94
- # pw-beta adjustment with attrs is to only represent severities *with* content
95
- # not all severities including those without content as empty lists
96
- attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
97
- self._notification = {} # pw-beta
98
- if notify := self.coordinator.data.gateway["notifications"]:
99
- for notify_id, details in notify.items(): # pw-beta uses notify_id
100
  for msg_type, msg in details.items():
101
  msg_type = msg_type.lower()
102
  if msg_type not in SEVERITIES:
103
- msg_type = "other" # pragma: no cover
104
-
105
- if (
106
- f"{msg_type}_msg" not in attrs
107
- ): # pw-beta Re-evaluate against Core
108
- attrs[f"{msg_type}_msg"] = []
109
  attrs[f"{msg_type}_msg"].append(msg)
110
 
111
- self._notification[
112
- notify_id
113
- ] = f"{msg_type.title()}: {msg}" # pw-beta
114
-
115
  return attrs
 
1
  """Plugwise Binary Sensor component for Home Assistant."""
 
2
 
3
  from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import Any, override
6
 
7
+ from plugwise.constants import BinarySensorType
 
 
 
8
 
9
+ from homeassistant.components.binary_sensor import (
10
+ BinarySensorDeviceClass,
11
+ BinarySensorEntity,
12
+ BinarySensorEntityDescription,
13
+ )
14
+ from homeassistant.const import EntityCategory
15
+ from homeassistant.core import HomeAssistant, callback
16
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
17
 
18
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
19
  from .entity import PlugwiseEntity
 
20
 
21
+ SEVERITIES = ["other", "info", "warning", "error"]
22
+
23
+ # Coordinator is used to centralize the data updates
24
  PARALLEL_UPDATES = 0
25
 
26
 
27
+ @dataclass(frozen=True)
28
+ class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription):
29
+ """Describes a Plugwise binary sensor entity."""
30
+
31
+ key: BinarySensorType
32
+
33
+
34
+ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
35
+ PlugwiseBinarySensorEntityDescription(
36
+ key="low_battery",
37
+ device_class=BinarySensorDeviceClass.BATTERY,
38
+ entity_category=EntityCategory.DIAGNOSTIC,
39
+ ),
40
+ PlugwiseBinarySensorEntityDescription(
41
+ key="compressor_state",
42
+ translation_key="compressor_state",
43
+ entity_category=EntityCategory.DIAGNOSTIC,
44
+ ),
45
+ PlugwiseBinarySensorEntityDescription(
46
+ key="cooling_enabled",
47
+ translation_key="cooling_enabled",
48
+ entity_category=EntityCategory.DIAGNOSTIC,
49
+ ),
50
+ PlugwiseBinarySensorEntityDescription(
51
+ key="dhw_state",
52
+ translation_key="dhw_state",
53
+ entity_category=EntityCategory.DIAGNOSTIC,
54
+ ),
55
+ PlugwiseBinarySensorEntityDescription(
56
+ key="flame_state",
57
+ translation_key="flame_state",
58
+ entity_category=EntityCategory.DIAGNOSTIC,
59
+ ),
60
+ PlugwiseBinarySensorEntityDescription(
61
+ key="heating_state",
62
+ translation_key="heating_state",
63
+ entity_category=EntityCategory.DIAGNOSTIC,
64
+ ),
65
+ PlugwiseBinarySensorEntityDescription(
66
+ key="cooling_state",
67
+ translation_key="cooling_state",
68
+ entity_category=EntityCategory.DIAGNOSTIC,
69
+ ),
70
+ PlugwiseBinarySensorEntityDescription(
71
+ key="secondary_boiler_state",
72
+ translation_key="secondary_boiler_state",
73
+ entity_category=EntityCategory.DIAGNOSTIC,
74
+ ),
75
+ PlugwiseBinarySensorEntityDescription(
76
+ key="plugwise_notification",
77
+ translation_key="plugwise_notification",
78
+ entity_category=EntityCategory.DIAGNOSTIC,
79
+ ),
80
+ )
81
+
82
+
83
  async def async_setup_entry(
84
  hass: HomeAssistant,
85
+ entry: PlugwiseConfigEntry,
86
+ async_add_entities: AddConfigEntryEntitiesCallback,
87
  ) -> None:
88
  """Set up the Smile binary_sensors from a config entry."""
89
+ coordinator = entry.runtime_data
90
+
91
+ @callback
92
+ def _add_entities() -> None:
93
+ """Add Entities."""
94
+ if not coordinator.new_devices:
95
+ return
96
+
97
+ async_add_entities(
98
+ PlugwiseBinarySensorEntity(coordinator, device_id, description)
99
+ for device_id in coordinator.new_devices
100
+ if (binary_sensors := coordinator.data[device_id].get("binary_sensors"))
101
+ for description in BINARY_SENSORS
102
+ if description.key in binary_sensors
103
+ )
104
+
105
+ _add_entities()
106
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
 
 
 
107
 
108
 
109
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
 
121
  super().__init__(coordinator, device_id)
122
  self.entity_description = description
123
  self._attr_unique_id = f"{device_id}-{description.key}"
 
124
 
125
  @property
126
+ @override
127
+ def is_on(self) -> bool:
128
  """Return true if the binary sensor is on."""
129
+ return self.device["binary_sensors"][self.entity_description.key]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  @property
132
+ @override
133
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
134
  """Return entity specific state attributes."""
135
  if self.entity_description.key != "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
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py RENAMED
@@ -1,7 +1,7 @@
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 (
7
  ATTR_HVAC_MODE,
@@ -12,53 +12,86 @@
12
  HVACAction,
13
  HVACMode,
14
  )
15
- from homeassistant.components.climate.const import (
16
- PRESET_AWAY, # pw-beta homekit emulation
17
- )
18
- from homeassistant.components.climate.const import (
19
- PRESET_HOME, # pw-beta homekit emulation
20
- )
21
- from homeassistant.config_entries import ConfigEntry
22
- from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
23
- from homeassistant.core import HomeAssistant
24
  from homeassistant.exceptions import HomeAssistantError
25
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
26
 
27
- from .const import CONF_HOMEKIT_EMULATION # pw-beta homekit emulation
28
- from .const import COORDINATOR # pw-beta
29
  from .const import DOMAIN, MASTER_THERMOSTATS
30
- from .coordinator import PlugwiseDataUpdateCoordinator
31
  from .entity import PlugwiseEntity
32
  from .util import plugwise_command
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  async def async_setup_entry(
36
  hass: HomeAssistant,
37
- config_entry: ConfigEntry,
38
- async_add_entities: AddEntitiesCallback,
39
  ) -> None:
40
  """Set up the Smile Thermostats from a config entry."""
41
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][
42
- config_entry.entry_id
43
- ][COORDINATOR]
44
-
45
- homekit_enabled: bool = config_entry.options.get(
46
- CONF_HOMEKIT_EMULATION, False
47
- ) # pw-beta homekit emulation
48
-
49
- async_add_entities(
50
- PlugwiseClimateEntity(
51
- coordinator, device_id, homekit_enabled
52
- ) # pw-beta homekit emulation
53
- for device_id, device in coordinator.data.devices.items()
54
- if device["dev_class"] in MASTER_THERMOSTATS
55
- )
 
 
 
 
 
 
 
 
56
 
57
 
58
- class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
59
- """Representation of an Plugwise thermostat."""
60
 
61
- _attr_has_entity_name = True
62
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
63
  _attr_translation_key = DOMAIN
64
 
@@ -66,46 +99,72 @@
66
  self,
67
  coordinator: PlugwiseDataUpdateCoordinator,
68
  device_id: str,
69
- homekit_enabled: bool, # pw-beta homekit emulation
70
  ) -> None:
71
  """Set up the Plugwise API."""
72
  super().__init__(coordinator, device_id)
73
- self._homekit_enabled = homekit_enabled # pw-beta homekit emulation
74
- self._homekit_mode: str | None = None # pw-beta homekit emulation
75
  self._attr_unique_id = f"{device_id}-climate"
76
 
 
 
 
 
 
 
 
 
 
77
  # Determine supported features
78
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
79
- if self.coordinator.data.gateway["cooling_present"]:
80
  self._attr_supported_features = (
81
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
82
  )
 
 
 
 
83
  if presets := self.device.get("preset_modes"):
84
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
85
  self._attr_preset_modes = presets
86
 
87
- # Determine hvac modes and current hvac mode
88
- self._attr_hvac_modes = [HVACMode.HEAT]
89
- if self.coordinator.data.gateway["cooling_present"]:
90
- self._attr_hvac_modes = [HVACMode.HEAT_COOL]
91
- if self.device["available_schedules"] != ["None"]:
92
- self._attr_hvac_modes.append(HVACMode.AUTO)
93
- if self._homekit_enabled: # pw-beta homekit emulation
94
- self._attr_hvac_modes.append(HVACMode.OFF) # pragma: no cover
95
-
96
  self._attr_min_temp = self.device["thermostat"]["lower_bound"]
97
- self._attr_max_temp = self.device["thermostat"]["upper_bound"]
98
  # Ensure we don't drop below 0.1
99
  self._attr_target_temperature_step = max(
100
  self.device["thermostat"]["resolution"], 0.1
101
  )
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  @property
104
- def current_temperature(self) -> float:
 
105
  """Return the current temperature."""
106
- return self.device["sensors"]["temperature"]
107
 
108
  @property
 
109
  def target_temperature(self) -> float:
110
  """Return the temperature we try to reach.
111
 
@@ -115,6 +174,7 @@
115
  return self.device["thermostat"]["setpoint"]
116
 
117
  @property
 
118
  def target_temperature_high(self) -> float:
119
  """Return the temperature we try to reach in case of cooling.
120
 
@@ -123,6 +183,7 @@
123
  return self.device["thermostat"]["setpoint_high"]
124
 
125
  @property
 
126
  def target_temperature_low(self) -> float:
127
  """Return the heating temperature we try to reach in case of heating.
128
 
@@ -131,92 +192,144 @@
131
  return self.device["thermostat"]["setpoint_low"]
132
 
133
  @property
 
134
  def hvac_mode(self) -> HVACMode:
135
- """Return HVAC operation ie. auto, heat, heat_cool, or off mode."""
136
  if (
137
- mode := self.device["mode"]
138
- ) is None or mode not in self.hvac_modes: # pw-beta add to Core
139
- return HVACMode.HEAT # pragma: no cover
140
- # pw-beta homekit emulation
141
- if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
142
- mode = HVACMode.OFF # pragma: no cover
143
-
144
  return HVACMode(mode)
145
 
146
  @property
147
- def hvac_action(self) -> HVACAction: # pw-beta add to Core
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  """Return the current running hvac operation if supported."""
149
- # When control_state is present, prefer this data
150
- if (control_state := self.device.get("control_state")) == "cooling":
151
- return HVACAction.COOLING
152
- # Support preheating state as heating,
153
- # until preheating is added as a separate state
154
- if control_state in ["heating", "preheating"]:
155
- return HVACAction.HEATING
156
- if control_state == "off":
157
- return HVACAction.IDLE
158
-
159
- hc_data = self.coordinator.data.devices[
160
- self.coordinator.data.gateway["heater_id"]
161
- ]
162
- if hc_data["binary_sensors"]["heating_state"]:
163
- return HVACAction.HEATING
164
- if hc_data["binary_sensors"].get("cooling_state", False): # pw-beta adds False
165
- return HVACAction.COOLING
166
 
167
  return HVACAction.IDLE
168
 
169
  @property
 
170
  def preset_mode(self) -> str | None:
171
  """Return the current preset mode."""
172
- return self.device["active_preset"]
173
 
174
  @plugwise_command
 
175
  async def async_set_temperature(self, **kwargs: Any) -> None:
176
  """Set new target temperature."""
177
- if ATTR_HVAC_MODE in kwargs: # pw-beta add to Core
178
- await self.async_set_hvac_mode(
179
- kwargs[ATTR_HVAC_MODE]
180
- ) # pw-beta add to Core
181
-
182
  data: dict[str, Any] = {}
183
  if ATTR_TEMPERATURE in kwargs:
184
- data["setpoint"] = kwargs[ATTR_TEMPERATURE]
185
  if ATTR_TARGET_TEMP_HIGH in kwargs:
186
- data["setpoint_high"] = kwargs[ATTR_TARGET_TEMP_HIGH]
187
  if ATTR_TARGET_TEMP_LOW in kwargs:
188
- data["setpoint_low"] = kwargs[ATTR_TARGET_TEMP_LOW]
189
 
190
- for temperature in data.values():
191
- if not (self._attr_min_temp <= temperature <= self._attr_max_temp):
192
- raise ValueError("Invalid temperature change requested")
193
 
194
- await self.coordinator.api.set_temperature(self.device["location"], data)
 
 
 
 
 
 
 
 
 
 
 
195
 
196
  @plugwise_command
 
197
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
198
- """Set the hvac mode."""
199
- if hvac_mode not in self.hvac_modes:
200
- raise HomeAssistantError("Unsupported hvac_mode")
201
-
202
- await self.coordinator.api.set_schedule_state(
203
- self.device["location"],
204
- self.device["last_used"],
205
- "on" if hvac_mode == HVACMode.AUTO else "off",
 
 
 
 
 
 
206
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
- # pw-beta: feature request - mimic HomeKit behavior
209
- self._homekit_mode = hvac_mode
210
- if self._homekit_enabled:
211
- if self._homekit_mode == HVACMode.OFF: # pragma: no cover
212
- await self.async_set_preset_mode(PRESET_AWAY) # pragma: no cover
213
- if (
214
- self._homekit_mode in [HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL]
215
- and self.device["active_preset"] == PRESET_AWAY
216
- ): # pragma: no cover
217
- await self.async_set_preset_mode(PRESET_HOME) # pragma: no cover
218
 
219
  @plugwise_command
 
220
  async def async_set_preset_mode(self, preset_mode: str) -> None:
221
  """Set the preset mode."""
222
- await self.coordinator.api.set_preset(self.device["location"], preset_mode)
 
1
  """Plugwise Climate component for Home Assistant."""
 
2
 
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Any, override
5
 
6
  from homeassistant.components.climate import (
7
  ATTR_HVAC_MODE,
 
12
  HVACAction,
13
  HVACMode,
14
  )
15
+ from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON, UnitOfTemperature
16
+ from homeassistant.core import HomeAssistant, callback
 
 
 
 
 
 
 
17
  from homeassistant.exceptions import HomeAssistantError
18
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
19
+ from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
20
 
 
 
21
  from .const import DOMAIN, MASTER_THERMOSTATS
22
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
23
  from .entity import PlugwiseEntity
24
  from .util import plugwise_command
25
 
26
+ ERROR_NO_SCHEDULE = "set_schedule_first"
27
+ PARALLEL_UPDATES = 0
28
+
29
+
30
+ def _check_for_schedule(active: bool, last_active: str | None) -> None:
31
+ """Raise a HAError when no thermostat schedule has been set."""
32
+ if not active and last_active is None:
33
+ raise HomeAssistantError(
34
+ translation_domain=DOMAIN,
35
+ translation_key=ERROR_NO_SCHEDULE,
36
+ )
37
+
38
+
39
+ @dataclass
40
+ class PlugwiseClimateExtraStoredData(ExtraStoredData):
41
+ """Object to hold extra stored data."""
42
+
43
+ last_active_schedule: str | None
44
+ previous_action_mode: str | None
45
+
46
+ @override
47
+ def as_dict(self) -> dict[str, Any]:
48
+ """Return a dict representation of the text data."""
49
+ return asdict(self)
50
+
51
+ @classmethod
52
+ def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData:
53
+ """Initialize a stored data object from a dict."""
54
+ return cls(
55
+ last_active_schedule=restored.get("last_active_schedule"),
56
+ previous_action_mode=restored.get("previous_action_mode"),
57
+ )
58
+
59
 
60
  async def async_setup_entry(
61
  hass: HomeAssistant,
62
+ entry: PlugwiseConfigEntry,
63
+ async_add_entities: AddConfigEntryEntitiesCallback,
64
  ) -> None:
65
  """Set up the Smile Thermostats from a config entry."""
66
+ coordinator = entry.runtime_data
67
+
68
+ @callback
69
+ def _add_entities() -> None:
70
+ """Add Entities."""
71
+ if not coordinator.new_devices:
72
+ return
73
+
74
+ if coordinator.api.smile.name == "Adam":
75
+ async_add_entities(
76
+ PlugwiseClimateEntity(coordinator, device_id)
77
+ for device_id in coordinator.new_devices
78
+ if coordinator.data[device_id]["dev_class"] == "climate"
79
+ )
80
+ else:
81
+ async_add_entities(
82
+ PlugwiseClimateEntity(coordinator, device_id)
83
+ for device_id in coordinator.new_devices
84
+ if coordinator.data[device_id]["dev_class"] in MASTER_THERMOSTATS
85
+ )
86
+
87
+ _add_entities()
88
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
89
 
90
 
91
+ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
92
+ """Representation of a Plugwise thermostat."""
93
 
94
+ _attr_name = None
95
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
96
  _attr_translation_key = DOMAIN
97
 
 
99
  self,
100
  coordinator: PlugwiseDataUpdateCoordinator,
101
  device_id: str,
 
102
  ) -> None:
103
  """Set up the Plugwise API."""
104
  super().__init__(coordinator, device_id)
 
 
105
  self._attr_unique_id = f"{device_id}-climate"
106
 
107
+ self._api = coordinator.api
108
+ gateway_id: str = self._api.gateway_id
109
+ self._gateway_data = coordinator.data[gateway_id]
110
+ self._last_active_schedule: str | None = None
111
+ self._location = device_id
112
+ if (location := self.device.get("location")) is not None:
113
+ self._location = location
114
+ self._previous_action_mode = HVACAction.HEATING.value
115
+
116
  # Determine supported features
117
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
118
+ if self._api.cooling_present and self._api.smile.name != "Adam":
119
  self._attr_supported_features = (
120
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
121
  )
122
+ if HVACMode.OFF in self.hvac_modes:
123
+ self._attr_supported_features |= (
124
+ ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
125
+ )
126
  if presets := self.device.get("preset_modes"):
127
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
128
  self._attr_preset_modes = presets
129
 
 
 
 
 
 
 
 
 
 
130
  self._attr_min_temp = self.device["thermostat"]["lower_bound"]
131
+ self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
132
  # Ensure we don't drop below 0.1
133
  self._attr_target_temperature_step = max(
134
  self.device["thermostat"]["resolution"], 0.1
135
  )
136
 
137
+ @override
138
+ async def async_added_to_hass(self) -> None:
139
+ """Run when entity about to be added."""
140
+ await super().async_added_to_hass()
141
+
142
+ if extra_data := await self.async_get_last_extra_data():
143
+ plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
144
+ extra_data.as_dict()
145
+ )
146
+ self._last_active_schedule = plugwise_extra_data.last_active_schedule
147
+ self._previous_action_mode = (
148
+ plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value
149
+ )
150
+
151
+ @property
152
+ @override
153
+ def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
154
+ """Return text specific state data to be restored."""
155
+ return PlugwiseClimateExtraStoredData(
156
+ last_active_schedule=self._last_active_schedule,
157
+ previous_action_mode=self._previous_action_mode,
158
+ )
159
+
160
  @property
161
+ @override
162
+ def current_temperature(self) -> float | None:
163
  """Return the current temperature."""
164
+ return self.device["sensors"].get("temperature")
165
 
166
  @property
167
+ @override
168
  def target_temperature(self) -> float:
169
  """Return the temperature we try to reach.
170
 
 
174
  return self.device["thermostat"]["setpoint"]
175
 
176
  @property
177
+ @override
178
  def target_temperature_high(self) -> float:
179
  """Return the temperature we try to reach in case of cooling.
180
 
 
183
  return self.device["thermostat"]["setpoint_high"]
184
 
185
  @property
186
+ @override
187
  def target_temperature_low(self) -> float:
188
  """Return the heating temperature we try to reach in case of heating.
189
 
 
192
  return self.device["thermostat"]["setpoint_low"]
193
 
194
  @property
195
+ @override
196
  def hvac_mode(self) -> HVACMode:
197
+ """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
198
  if (
199
+ mode := self.device.get("climate_mode")
200
+ ) is None or mode not in self.hvac_modes:
201
+ return HVACMode.HEAT
 
 
 
 
202
  return HVACMode(mode)
203
 
204
  @property
205
+ @override
206
+ def hvac_modes(self) -> list[HVACMode]:
207
+ """Return a list of available HVACModes."""
208
+ hvac_modes: list[HVACMode] = []
209
+ if "regulation_modes" in self._gateway_data:
210
+ hvac_modes.append(HVACMode.OFF)
211
+
212
+ if self.device.get("available_schedules"):
213
+ hvac_modes.append(HVACMode.AUTO)
214
+
215
+ if self._api.cooling_present:
216
+ if "regulation_modes" in self._gateway_data:
217
+ if "heating" in self._gateway_data["regulation_modes"]:
218
+ hvac_modes.append(HVACMode.HEAT)
219
+ if "cooling" in self._gateway_data["regulation_modes"]:
220
+ hvac_modes.append(HVACMode.COOL)
221
+ else:
222
+ hvac_modes.append(HVACMode.HEAT_COOL)
223
+ else:
224
+ hvac_modes.append(HVACMode.HEAT)
225
+
226
+ return hvac_modes
227
+
228
+ @property
229
+ @override
230
+ def hvac_action(self) -> HVACAction:
231
  """Return the current running hvac operation if supported."""
232
+ # Keep track of the previous hvac_action mode.
233
+ # When no cooling available, _previous_action_mode is always heating
234
+ if (
235
+ "regulation_modes" in self._gateway_data
236
+ and HVACAction.COOLING.value in self._gateway_data["regulation_modes"]
237
+ ):
238
+ mode = self._gateway_data["select_regulation_mode"]
239
+ if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
240
+ self._previous_action_mode = mode
241
+
242
+ if (action := self.device.get("control_state")) is not None:
243
+ return HVACAction(action)
 
 
 
 
 
244
 
245
  return HVACAction.IDLE
246
 
247
  @property
248
+ @override
249
  def preset_mode(self) -> str | None:
250
  """Return the current preset mode."""
251
+ return self.device.get("active_preset")
252
 
253
  @plugwise_command
254
+ @override
255
  async def async_set_temperature(self, **kwargs: Any) -> None:
256
  """Set new target temperature."""
 
 
 
 
 
257
  data: dict[str, Any] = {}
258
  if ATTR_TEMPERATURE in kwargs:
259
+ data["setpoint"] = kwargs.get(ATTR_TEMPERATURE)
260
  if ATTR_TARGET_TEMP_HIGH in kwargs:
261
+ data["setpoint_high"] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
262
  if ATTR_TARGET_TEMP_LOW in kwargs:
263
+ data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
264
 
265
+ if mode := kwargs.get(ATTR_HVAC_MODE):
266
+ await self.async_set_hvac_mode(mode)
 
267
 
268
+ await self._api.set_temperature(self._location, data)
269
+
270
+ def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
271
+ """Return the API regulation value for a manual HVAC mode, or None.
272
+
273
+ The function inputs are limited to the HVACModes HEAT and COOL.
274
+ """
275
+ if hvac_mode == HVACMode.HEAT:
276
+ mode = HVACAction.HEATING.value
277
+ if hvac_mode == HVACMode.COOL:
278
+ mode = HVACAction.COOLING.value
279
+ return mode
280
 
281
  @plugwise_command
282
+ @override
283
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
284
+ """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
285
+ # Early exit if no mode change
286
+ if hvac_mode == self.hvac_mode:
287
+ return
288
+
289
+ # Adam only: set to HVACMode.OFF
290
+ if hvac_mode == HVACMode.OFF:
291
+ await self._api.set_regulation_mode(hvac_mode.value)
292
+ return
293
+
294
+ current_schedule = self.device.get("select_schedule")
295
+ schedule_is_active = current_schedule not in (None, "off")
296
+ desired_schedule = (
297
+ current_schedule if schedule_is_active else self._last_active_schedule
298
  )
299
+ # Adam only: transition from HVACMode.OFF
300
+ if self.hvac_mode == HVACMode.OFF:
301
+ if hvac_mode == HVACMode.AUTO:
302
+ _check_for_schedule(schedule_is_active, self._last_active_schedule)
303
+ await self._api.set_schedule_state(
304
+ self._location, STATE_ON, desired_schedule
305
+ )
306
+ await self._api.set_regulation_mode(self._previous_action_mode)
307
+ return
308
+
309
+ # Transition to manual mode
310
+ if schedule_is_active:
311
+ await self._api.set_schedule_state(
312
+ self._location, STATE_OFF, current_schedule
313
+ )
314
+ self._last_active_schedule = current_schedule
315
+ regulation = self._regulation_mode_for_hvac(hvac_mode)
316
+ await self._api.set_regulation_mode(regulation)
317
+ return
318
+
319
+ # Common - transition from auto = schedule off
320
+ if self.hvac_mode == HVACMode.AUTO:
321
+ await self._api.set_schedule_state(
322
+ self._location, STATE_OFF, current_schedule
323
+ )
324
+ self._last_active_schedule = current_schedule
325
+ return
326
 
327
+ # Common - transition to auto = schedule on
328
+ _check_for_schedule(schedule_is_active, self._last_active_schedule)
329
+ await self._api.set_schedule_state(self._location, STATE_ON, desired_schedule)
 
 
 
 
 
 
 
330
 
331
  @plugwise_command
332
+ @override
333
  async def async_set_preset_mode(self, preset_mode: str) -> None:
334
  """Set the preset mode."""
335
+ await self._api.set_preset(self._location, preset_mode)
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py RENAMED
@@ -1,114 +1,79 @@
1
  """Config flow for Plugwise integration."""
2
- from __future__ import annotations
3
 
4
- import datetime as dt # pw-beta options
5
- from typing import Any
6
 
 
 
 
 
 
 
 
 
 
7
  import voluptuous as vol
8
 
9
- from homeassistant import config_entries
10
- from homeassistant.components.zeroconf import ZeroconfServiceInfo
11
- from homeassistant.config_entries import ConfigEntry, ConfigFlow
12
  from homeassistant.const import (
 
13
  CONF_BASE,
14
  CONF_HOST,
15
  CONF_NAME,
16
  CONF_PASSWORD,
17
  CONF_PORT,
18
- CONF_SCAN_INTERVAL,
19
  CONF_USERNAME,
20
  )
21
- from homeassistant.core import HomeAssistant, callback
22
- from homeassistant.data_entry_flow import FlowResult
23
- from homeassistant.helpers import config_validation as cv
24
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
25
- from plugwise.exceptions import (
26
- ConnectionFailedError,
27
- InvalidAuthentication,
28
- InvalidSetupError,
29
- InvalidXMLError,
30
- ResponseError,
31
- UnsupportedDeviceError,
32
- )
33
-
34
- # pw-beta Note; the below are explicit through isort
35
- from plugwise import Smile
36
 
37
  from .const import (
38
- API,
39
- COORDINATOR,
40
  DEFAULT_PORT,
41
  DEFAULT_USERNAME,
42
  DOMAIN,
43
  FLOW_SMILE,
44
  FLOW_STRETCH,
45
- PW_TYPE,
46
  SMILE,
 
 
47
  STRETCH,
48
  STRETCH_USERNAME,
 
49
  ZEROCONF_MAP,
50
  )
51
 
52
- # pw-beta Note; the below are explicit through isort
53
- from .const import CONF_HOMEKIT_EMULATION # pw-beta option
54
- from .const import CONF_REFRESH_INTERVAL # pw-beta option
55
- from .const import CONF_USB_PATH # pw-beta usb
56
- from .const import DEFAULT_SCAN_INTERVAL # pw-beta option
57
- from .const import FLOW_NET # pw-beta usb
58
- from .const import FLOW_TYPE # pw-beta usb
59
- from .const import FLOW_USB # pw-beta usb
60
- from .const import STICK # pw-beta usb
61
-
62
- CONNECTION_SCHEMA = vol.Schema(
63
- {vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In([FLOW_NET, FLOW_USB])}
64
- ) # pw-beta usb
65
-
66
-
67
- @callback
68
- def plugwise_stick_entries(hass): # pw-beta usb
69
- """Return existing connections for Plugwise USB-stick domain."""
70
- sticks = []
71
- for entry in hass.config_entries.async_entries(DOMAIN):
72
- if entry.data.get(PW_TYPE) == STICK:
73
- sticks.append(entry.data.get(CONF_USB_PATH))
74
- return sticks
75
-
76
-
77
- def _base_gw_schema(
78
- discovery_info: ZeroconfServiceInfo | None,
79
- user_input: dict[str, Any] | None,
80
- ) -> vol.Schema:
81
  """Generate base schema for gateways."""
 
 
82
  if not discovery_info:
83
- if not user_input:
84
- return vol.Schema(
85
- {
86
- vol.Required(CONF_PASSWORD): str,
87
- vol.Required(CONF_HOST): str,
88
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
89
- vol.Required(CONF_USERNAME, default=SMILE): vol.In(
90
- {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
91
- ),
92
- }
93
- )
94
- return vol.Schema(
95
  {
96
- vol.Required(CONF_PASSWORD, default=user_input[CONF_PASSWORD]): str,
97
- vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str,
98
- vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): int,
99
- vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): vol.In(
100
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
101
  ),
102
  }
103
  )
104
 
105
- return vol.Schema({vol.Required(CONF_PASSWORD): str})
106
 
107
 
108
- async def validate_gw_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
109
  """Validate whether the user input allows us to connect to the gateway.
110
 
111
- Data has the keys from _base_gw_schema() with values provided by the user.
112
  """
113
  websession = async_get_clientsession(hass, verify_ssl=False)
114
  api = Smile(
@@ -116,24 +81,51 @@
116
  password=data[CONF_PASSWORD],
117
  port=data[CONF_PORT],
118
  username=data[CONF_USERNAME],
119
- timeout=30,
120
  websession=websession,
121
  )
122
  await api.connect()
123
  return api
124
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
127
  """Handle a config flow for Plugwise Smile."""
128
 
129
  VERSION = 1
130
 
131
  discovery_info: ZeroconfServiceInfo | None = None
 
132
  _username: str = DEFAULT_USERNAME
133
 
 
134
  async def async_step_zeroconf(
135
  self, discovery_info: ZeroconfServiceInfo
136
- ) -> FlowResult:
137
  """Prepare configuration for a discovered Plugwise Smile."""
138
  self.discovery_info = discovery_info
139
  _properties = discovery_info.properties
@@ -141,7 +133,7 @@
141
  unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
142
  if config_entry := await self.async_set_unique_id(unique_id):
143
  try:
144
- await validate_gw_input(
145
  self.hass,
146
  {
147
  CONF_HOST: discovery_info.host,
@@ -150,7 +142,7 @@
150
  CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
151
  },
152
  )
153
- except Exception: # pylint: disable=broad-except
154
  self._abort_if_unique_id_configured()
155
  else:
156
  self._abort_if_unique_id_configured(
@@ -162,184 +154,108 @@
162
 
163
  if DEFAULT_USERNAME not in unique_id:
164
  self._username = STRETCH_USERNAME
165
- _product = _properties.get("product", None)
166
  _version = _properties.get("version", "n/a")
167
  _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
168
 
169
  # This is an Anna, but we already have config entries.
170
  # Assuming that the user has already configured Adam, aborting discovery.
171
- if self._async_current_entries() and _product == "smile_thermo":
172
- return self.async_abort(reason="anna_with_adam")
173
 
174
  # If we have discovered an Adam or Anna, both might be on the network.
175
  # In that case, we need to cancel the Anna flow, as the Adam should
176
  # be added.
177
- for flow in self._async_in_progress():
178
- # This is an Anna, and there is already an Adam flow in progress
179
- if (
180
- _product == "smile_thermo"
181
- and "context" in flow
182
- and flow["context"].get("product") == "smile_open_therm"
183
- ):
184
- return self.async_abort(reason="anna_with_adam")
185
-
186
- # This is an Adam, and there is already an Anna flow in progress
187
- if (
188
- _product == "smile_open_therm"
189
- and "context" in flow
190
- and flow["context"].get("product") == "smile_thermo"
191
- and "flow_id" in flow
192
- ):
193
- self.hass.config_entries.flow.async_abort(flow["flow_id"])
194
 
195
  self.context.update(
196
  {
197
- "title_placeholders": {
198
- CONF_HOST: discovery_info.host,
199
- CONF_NAME: _name,
200
- CONF_PORT: discovery_info.port,
201
- CONF_USERNAME: self._username,
202
- },
203
- "configuration_url": f"http://{discovery_info.host}:{discovery_info.port}",
204
- "product": _product,
205
  }
206
  )
207
- return await self.async_step_user_gateway()
208
 
209
- async def async_step_user_gateway(
210
- self, user_input: dict[str, Any] | None = None
211
- ) -> FlowResult:
212
- """Handle the initial step when using network/gateway setups."""
213
- errors: dict[str, str] = {}
 
 
 
 
 
214
 
215
- if not user_input:
216
- return self.async_show_form(
217
- step_id="user_gateway",
218
- data_schema=_base_gw_schema(self.discovery_info, None),
219
- errors=errors,
220
- )
221
-
222
- if self.discovery_info:
223
- user_input[CONF_HOST] = self.discovery_info.host
224
- user_input[CONF_PORT] = self.discovery_info.port
225
- user_input[CONF_USERNAME] = self._username
226
- try:
227
- api = await validate_gw_input(self.hass, user_input)
228
- except ConnectionFailedError:
229
- errors[CONF_BASE] = "cannot_connect"
230
- except InvalidAuthentication:
231
- errors[CONF_BASE] = "invalid_auth"
232
- except InvalidSetupError:
233
- errors[CONF_BASE] = "invalid_setup"
234
- except (InvalidXMLError, ResponseError):
235
- errors[CONF_BASE] = "response_error"
236
- except UnsupportedDeviceError:
237
- errors[CONF_BASE] = "unsupported"
238
- except Exception: # pylint: disable=broad-except
239
- errors[CONF_BASE] = "unknown"
240
-
241
- if errors:
242
- return self.async_show_form(
243
- step_id="user_gateway",
244
- data_schema=_base_gw_schema(None, user_input),
245
- errors=errors,
246
- )
247
-
248
- await self.async_set_unique_id(
249
- api.smile_hostname or api.gateway_id, raise_on_progress=False
250
- )
251
- self._abort_if_unique_id_configured()
252
-
253
- user_input[PW_TYPE] = API
254
- return self.async_create_entry(title=api.smile_name, data=user_input)
255
 
 
256
  async def async_step_user(
257
  self, user_input: dict[str, Any] | None = None
258
- ) -> FlowResult:
259
  """Handle the initial step when using network/gateway setups."""
260
  errors: dict[str, str] = {}
261
- if user_input is not None:
262
- if user_input[FLOW_TYPE] == FLOW_NET:
263
- return await self.async_step_user_gateway()
264
 
265
- if user_input[FLOW_TYPE] == FLOW_USB:
266
- return await self.async_step_user_usb()
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  return self.async_show_form(
269
- step_id="user",
270
- data_schema=CONNECTION_SCHEMA,
271
  errors=errors,
272
  )
273
 
274
- @staticmethod
275
- @callback
276
- def async_get_options_flow(
277
- config_entry: ConfigEntry,
278
- ) -> config_entries.OptionsFlow: # pw-beta options
279
- """Get the options flow for this handler."""
280
- return PlugwiseOptionsFlowHandler(config_entry)
281
-
282
-
283
- # pw-beta - change the scan-interval via CONFIGURE
284
- # pw-beta - add homekit emulation via CONFIGURE
285
- # pw-beta - change the frontend refresh interval via CONFIGURE
286
- class PlugwiseOptionsFlowHandler(config_entries.OptionsFlow): # pw-beta options
287
- """Plugwise option flow."""
288
-
289
- def __init__(self, config_entry: ConfigEntry) -> None: # pragma: no cover
290
- """Initialize options flow."""
291
- self.config_entry = config_entry
292
-
293
- async def async_step_none(
294
  self, user_input: dict[str, Any] | None = None
295
- ) -> FlowResult: # pragma: no cover
296
- """No options available."""
297
- if user_input is not None:
298
- # Apparently not possible to abort an options flow at the moment
299
- return self.async_create_entry(title="", data=self.config_entry.options)
300
-
301
- return self.async_show_form(step_id="none")
302
-
303
- async def async_step_init(
304
- self, user_input: dict[str, Any] | None = None
305
- ) -> FlowResult: # pragma: no cover
306
- """Manage the Plugwise options."""
307
- if not self.config_entry.data.get(CONF_HOST):
308
- return await self.async_step_none(user_input)
309
-
310
- if user_input is not None:
311
- return self.async_create_entry(title="", data=user_input)
312
-
313
- coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id][COORDINATOR]
314
- interval: dt.timedelta = DEFAULT_SCAN_INTERVAL[
315
- coordinator.api.smile_type
316
- ] # pw-beta options
317
-
318
- data = {
319
- vol.Optional(
320
- CONF_SCAN_INTERVAL,
321
- default=self.config_entry.options.get(
322
- CONF_SCAN_INTERVAL, interval.seconds
323
- ),
324
- ): vol.All(cv.positive_int, vol.Clamp(min=10)),
325
- } # pw-beta
326
 
327
- if coordinator.api.smile_type != "thermostat":
328
- return self.async_show_form(step_id="init", data_schema=vol.Schema(data))
329
 
330
- data.update(
331
- {
332
- vol.Optional(
333
- CONF_HOMEKIT_EMULATION,
334
- default=self.config_entry.options.get(
335
- CONF_HOMEKIT_EMULATION, False
336
- ),
337
- ): cv.boolean,
338
- vol.Optional(
339
- CONF_REFRESH_INTERVAL,
340
- default=self.config_entry.options.get(CONF_REFRESH_INTERVAL, 1.5),
341
- ): vol.All(vol.Coerce(float), vol.Range(min=1.5, max=10.0)),
342
  }
343
- ) # pw-beta
344
 
345
- return self.async_show_form(step_id="init", data_schema=vol.Schema(data))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Config flow for Plugwise integration."""
 
2
 
3
+ import logging
4
+ from typing import Any, Self, override
5
 
6
+ from plugwise import Smile
7
+ from plugwise.exceptions import (
8
+ ConnectionFailedError,
9
+ InvalidAuthentication,
10
+ InvalidSetupError,
11
+ InvalidXMLError,
12
+ ResponseError,
13
+ UnsupportedDeviceError,
14
+ )
15
  import voluptuous as vol
16
 
17
+ from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
 
 
18
  from homeassistant.const import (
19
+ ATTR_CONFIGURATION_URL,
20
  CONF_BASE,
21
  CONF_HOST,
22
  CONF_NAME,
23
  CONF_PASSWORD,
24
  CONF_PORT,
 
25
  CONF_USERNAME,
26
  )
27
+ from homeassistant.core import HomeAssistant
 
 
28
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
29
+ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
 
 
 
 
 
 
 
 
 
 
30
 
31
  from .const import (
32
+ ANNA_WITH_ADAM,
 
33
  DEFAULT_PORT,
34
  DEFAULT_USERNAME,
35
  DOMAIN,
36
  FLOW_SMILE,
37
  FLOW_STRETCH,
 
38
  SMILE,
39
+ SMILE_OPEN_THERM,
40
+ SMILE_THERMO,
41
  STRETCH,
42
  STRETCH_USERNAME,
43
+ UNKNOWN_SMILE,
44
  ZEROCONF_MAP,
45
  )
46
 
47
+ _LOGGER = logging.getLogger(__name__)
48
+
49
+ SMILE_RECONF_SCHEMA = vol.Schema(
50
+ {
51
+ vol.Required(CONF_HOST): str,
52
+ }
53
+ )
54
+
55
+
56
+ def smile_user_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  """Generate base schema for gateways."""
58
+ schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
59
+
60
  if not discovery_info:
61
+ schema = schema.extend(
 
 
 
 
 
 
 
 
 
 
 
62
  {
63
+ vol.Required(CONF_HOST): str,
64
+ vol.Required(CONF_USERNAME, default=SMILE): vol.In(
 
 
65
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
66
  ),
67
  }
68
  )
69
 
70
+ return schema
71
 
72
 
73
+ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
74
  """Validate whether the user input allows us to connect to the gateway.
75
 
76
+ Data has the keys from the schema with values provided by the user.
77
  """
78
  websession = async_get_clientsession(hass, verify_ssl=False)
79
  api = Smile(
 
81
  password=data[CONF_PASSWORD],
82
  port=data[CONF_PORT],
83
  username=data[CONF_USERNAME],
 
84
  websession=websession,
85
  )
86
  await api.connect()
87
  return api
88
 
89
 
90
+ async def verify_connection(
91
+ hass: HomeAssistant, user_input: dict[str, Any]
92
+ ) -> tuple[Smile | None, dict[str, str]]:
93
+ """Verify and return the gateway connection or an error."""
94
+ errors: dict[str, str] = {}
95
+
96
+ try:
97
+ return (await validate_input(hass, user_input), errors)
98
+ except ConnectionFailedError:
99
+ errors[CONF_BASE] = "cannot_connect"
100
+ except InvalidAuthentication:
101
+ errors[CONF_BASE] = "invalid_auth"
102
+ except InvalidSetupError:
103
+ errors[CONF_BASE] = "invalid_setup"
104
+ except InvalidXMLError, ResponseError:
105
+ errors[CONF_BASE] = "response_error"
106
+ except UnsupportedDeviceError:
107
+ errors[CONF_BASE] = "unsupported"
108
+ except Exception:
109
+ _LOGGER.exception(
110
+ "Unknown exception while verifying connection with your Plugwise Smile"
111
+ )
112
+ errors[CONF_BASE] = "unknown"
113
+ return (None, errors)
114
+
115
+
116
  class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
117
  """Handle a config flow for Plugwise Smile."""
118
 
119
  VERSION = 1
120
 
121
  discovery_info: ZeroconfServiceInfo | None = None
122
+ product: str = UNKNOWN_SMILE
123
  _username: str = DEFAULT_USERNAME
124
 
125
+ @override
126
  async def async_step_zeroconf(
127
  self, discovery_info: ZeroconfServiceInfo
128
+ ) -> ConfigFlowResult:
129
  """Prepare configuration for a discovered Plugwise Smile."""
130
  self.discovery_info = discovery_info
131
  _properties = discovery_info.properties
 
133
  unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
134
  if config_entry := await self.async_set_unique_id(unique_id):
135
  try:
136
+ await validate_input(
137
  self.hass,
138
  {
139
  CONF_HOST: discovery_info.host,
 
142
  CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
143
  },
144
  )
145
+ except Exception: # noqa: BLE001
146
  self._abort_if_unique_id_configured()
147
  else:
148
  self._abort_if_unique_id_configured(
 
154
 
155
  if DEFAULT_USERNAME not in unique_id:
156
  self._username = STRETCH_USERNAME
157
+ self.product = _product = _properties.get("product", UNKNOWN_SMILE)
158
  _version = _properties.get("version", "n/a")
159
  _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
160
 
161
  # This is an Anna, but we already have config entries.
162
  # Assuming that the user has already configured Adam, aborting discovery.
163
+ if self._async_current_entries() and _product == SMILE_THERMO:
164
+ return self.async_abort(reason=ANNA_WITH_ADAM)
165
 
166
  # If we have discovered an Adam or Anna, both might be on the network.
167
  # In that case, we need to cancel the Anna flow, as the Adam should
168
  # be added.
169
+ if self.hass.config_entries.flow.async_has_matching_flow(self):
170
+ return self.async_abort(reason=ANNA_WITH_ADAM)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
  self.context.update(
173
  {
174
+ "title_placeholders": {CONF_NAME: _name},
175
+ ATTR_CONFIGURATION_URL: (
176
+ f"http://{discovery_info.host}:{discovery_info.port}"
177
+ ),
 
 
 
 
178
  }
179
  )
180
+ return await self.async_step_user()
181
 
182
+ @override
183
+ def is_matching(self, other_flow: Self) -> bool:
184
+ """Return True if other_flow is matching this flow."""
185
+ # This is an Anna, and there is already an Adam flow in progress
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
+ @override
196
  async def async_step_user(
197
  self, user_input: dict[str, Any] | None = None
198
+ ) -> ConfigFlowResult:
199
  """Handle the initial step when using network/gateway setups."""
200
  errors: dict[str, str] = {}
 
 
 
201
 
202
+ if user_input is not None:
203
+ user_input[CONF_PORT] = DEFAULT_PORT
204
+ if self.discovery_info:
205
+ user_input[CONF_HOST] = self.discovery_info.host
206
+ user_input[CONF_PORT] = self.discovery_info.port
207
+ user_input[CONF_USERNAME] = self._username
208
+
209
+ api, errors = await verify_connection(self.hass, user_input)
210
+ if api:
211
+ await self.async_set_unique_id(
212
+ api.smile.hostname or api.gateway_id,
213
+ raise_on_progress=False,
214
+ )
215
+ self._abort_if_unique_id_configured()
216
+ return self.async_create_entry(title=api.smile.name, data=user_input)
217
 
218
  return self.async_show_form(
219
+ step_id=SOURCE_USER,
220
+ data_schema=smile_user_schema(self.discovery_info),
221
  errors=errors,
222
  )
223
 
224
+ async def async_step_reconfigure(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  self, user_input: dict[str, Any] | None = None
226
+ ) -> ConfigFlowResult:
227
+ """Handle reconfiguration of the integration."""
228
+ errors: dict[str, str] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
+ reconfigure_entry = self._get_reconfigure_entry()
 
231
 
232
+ if user_input:
233
+ # Keep current username and password
234
+ full_input = {
235
+ CONF_HOST: user_input.get(CONF_HOST),
236
+ CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
237
+ CONF_USERNAME: reconfigure_entry.data.get(CONF_USERNAME),
238
+ CONF_PASSWORD: reconfigure_entry.data.get(CONF_PASSWORD),
 
 
 
 
 
239
  }
 
240
 
241
+ api, errors = await verify_connection(self.hass, full_input)
242
+ if api:
243
+ await self.async_set_unique_id(
244
+ api.smile.hostname or api.gateway_id,
245
+ raise_on_progress=False,
246
+ )
247
+ self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
248
+ return self.async_update_reload_and_abort(
249
+ reconfigure_entry,
250
+ data_updates=full_input,
251
+ )
252
+
253
+ return self.async_show_form(
254
+ step_id="reconfigure",
255
+ data_schema=self.add_suggested_values_to_schema(
256
+ data_schema=SMILE_RECONF_SCHEMA,
257
+ suggested_values=reconfigure_entry.data,
258
+ ),
259
+ description_placeholders={"title": reconfigure_entry.title},
260
+ errors=errors,
261
+ )
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py RENAMED
@@ -1,70 +1,42 @@
1
  """Constants for Plugwise component."""
 
2
  from datetime import timedelta
3
  import logging
4
- from typing import Final
5
-
6
- import voluptuous as vol # pw-beta usb
7
 
8
  from homeassistant.const import Platform
9
- from homeassistant.helpers import config_validation as cv
10
 
11
  DOMAIN: Final = "plugwise"
12
 
13
  LOGGER = logging.getLogger(__package__)
14
 
 
15
  API: Final = "api"
16
- COORDINATOR: Final = "coordinator"
17
- CONF_HOMEKIT_EMULATION: Final = "homekit_emulation" # pw-beta options
18
- CONF_REFRESH_INTERVAL: Final = "refresh_interval" # pw-beta options
19
- CONF_MANUAL_PATH: Final = "Enter Manually"
 
20
  GATEWAY: Final = "gateway"
 
21
  PW_TYPE: Final = "plugwise_type"
 
22
  SMILE: Final = "smile"
23
- STICK: Final = "stick"
 
24
  STRETCH: Final = "stretch"
25
  STRETCH_USERNAME: Final = "stretch"
26
- USB: Final = "usb"
27
-
28
- FLOW_NET: Final = "Network: Smile/Stretch"
29
- FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
30
- FLOW_STRETCH: Final = "Stretch (Stretch)"
31
- FLOW_TYPE: Final = "flow_type"
32
- FLOW_USB: Final = "USB: Stick"
33
 
34
- UNDO_UPDATE_LISTENER: Final = "undo_update_listener"
35
-
36
- # Default directives
37
- DEFAULT_PORT: Final = 80
38
- DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
39
- "power": timedelta(seconds=10),
40
- "stretch": timedelta(seconds=60),
41
- "thermostat": timedelta(seconds=60),
42
- }
43
- DEFAULT_TIMEOUT: Final = 10
44
- DEFAULT_USERNAME: Final = "smile"
45
-
46
- # --- Const for Plugwise Smile and Stretch
47
- PLATFORMS_GATEWAY: Final[list[str]] = [
48
  Platform.BINARY_SENSOR,
 
49
  Platform.CLIMATE,
50
  Platform.NUMBER,
51
  Platform.SELECT,
52
  Platform.SENSOR,
53
  Platform.SWITCH,
54
  ]
55
- SENSOR_PLATFORMS: Final[list[str]] = [Platform.SENSOR, Platform.SWITCH]
56
- SERVICE_DELETE: Final = "delete_notification"
57
- SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
58
-
59
- # Climate const:
60
- MASTER_THERMOSTATS: Final[list[str]] = [
61
- "thermostat",
62
- "zone_thermometer",
63
- "zone_thermostat",
64
- "thermostatic_radiator_valve",
65
- ]
66
-
67
- # Config_flow const:
68
  ZEROCONF_MAP: Final[dict[str, str]] = {
69
  "smile": "Smile P1",
70
  "smile_thermo": "Smile Anna",
@@ -72,149 +44,45 @@
72
  "stretch": "Stretch",
73
  }
74
 
75
- # Binary Sensors:
76
- COOLING: Final = "cooling_enabled"
77
- COMPRESSOR_STATE: Final = "compressor_state"
78
- DHW_STATE: Final = "dhw_state"
79
- FLAME_STATE: Final = "flame_state"
80
- PW_NOTIFICATION: Final = "plugwise_notification"
81
- SLAVE_BOILER_STATE: Final = "slave_boiler_state"
82
-
83
- # Sensors:
84
- BATTERY: Final = "battery"
85
- CURRENT_TEMP: Final = "temperature"
86
- DEVICE_STATE: Final = "device_state"
87
- DHW_TEMP: Final = "dhw_temperature"
88
- EL_CONSUMED: Final = "electricity_consumed"
89
- EL_CONSUMED_INTERVAL: Final = "electricity_consumed_interval"
90
- EL_CONSUMED_OFF_PEAK_CUMULATIVE: Final = "electricity_consumed_off_peak_cumulative"
91
- EL_CONSUMED_OFF_PEAK_INTERVAL: Final = "electricity_consumed_off_peak_interval"
92
- EL_CONSUMED_OFF_PEAK_POINT: Final = "electricity_consumed_off_peak_point"
93
- EL_CONSUMED_PEAK_CUMULATIVE: Final = "electricity_consumed_peak_cumulative"
94
- EL_CONSUMED_PEAK_INTERVAL: Final = "electricity_consumed_peak_interval"
95
- EL_CONSUMED_PEAK_POINT: Final = "electricity_consumed_peak_point"
96
- EL_CONSUMED_POINT: Final = "electricity_consumed_point"
97
- EL_PHASE_ONE_CONSUMED: Final = "electricity_phase_one_consumed"
98
- EL_PHASE_TWO_CONSUMED: Final = "electricity_phase_two_consumed"
99
- EL_PHASE_THREE_CONSUMED: Final = "electricity_phase_three_consumed"
100
- EL_PHASE_ONE_PRODUCED: Final = "electricity_phase_one_produced"
101
- EL_PHASE_TWO_PRODUCED: Final = "electricity_phase_two_produced"
102
- EL_PHASE_THREE_PRODUCED: Final = "electricity_phase_three_produced"
103
- EL_PRODUCED: Final = "electricity_produced"
104
- EL_PRODUCED_INTERVAL: Final = "electricity_produced_interval"
105
- EL_PRODUCED_OFF_PEAK_CUMULATIVE: Final = "electricity_produced_off_peak_cumulative"
106
- EL_PRODUCED_OFF_PEAK_INTERVAL: Final = "electricity_produced_off_peak_interval"
107
- EL_PRODUCED_OFF_PEAK_POINT: Final = "electricity_produced_off_peak_point"
108
- EL_PRODUCED_PEAK_CUMULATIVE: Final = "electricity_produced_peak_cumulative"
109
- EL_PRODUCED_PEAK_INTERVAL: Final = "electricity_produced_peak_interval"
110
- EL_PRODUCED_PEAK_POINT: Final = "electricity_produced_peak_point"
111
- EL_PRODUCED_POINT: Final = "electricity_produced_point"
112
- GAS_CONSUMED_CUMULATIVE: Final = "gas_consumed_cumulative"
113
- GAS_CONSUMED_INTERVAL: Final = "gas_consumed_interval"
114
- HUMIDITY: Final = "humidity"
115
- INTENDED_BOILER_TEMP: Final = "intended_boiler_temperature"
116
- MOD_LEVEL: Final = "modulation_level"
117
- NET_EL_CUMULATIVE: Final = "net_electricity_cumulative"
118
- NET_EL_POINT: Final = "net_electricity_point"
119
- OUTDOOR_AIR_TEMP: Final = "outdoor_air_temperature"
120
- OUTDOOR_TEMP: Final = "outdoor_temperature"
121
- RETURN_TEMP: Final = "return_temperature"
122
- TARGET_TEMP: Final = "setpoint"
123
- TARGET_TEMP_HIGH: Final = "setpoint_high"
124
- TARGET_TEMP_LOW: Final = "setpoint_low"
125
- TEMP_DIFF: Final = "temperature_difference"
126
- VALVE_POS: Final = "valve_position"
127
- V_PHASE_ONE: Final = "voltage_phase_one"
128
- V_PHASE_TWO: Final = "voltage_phase_two"
129
- V_PHASE_THREE: Final = "voltage_phase_three"
130
- WATER_PRESSURE: Final = "water_pressure"
131
- WATER_TEMP: Final = "water_temperature"
132
-
133
- # Switches
134
- COOLING_ENABLED = "cooling_ena_switch"
135
- DHW_COMF_MODE: Final = "dhw_cm_switch"
136
- LOCK: Final = "lock"
137
- RELAY: Final = "relay"
138
-
139
-
140
- # --- Const for Plugwise USB-stick.
141
-
142
- PLATFORMS_USB: Final[list[str]] = [
143
- Platform.BINARY_SENSOR,
144
- Platform.SENSOR,
145
- Platform.SWITCH,
146
  ]
147
- CONF_USB_PATH: Final = "usb_path"
148
-
149
- # Callback types
150
- CB_NEW_NODE: Final = "NEW_NODE"
151
- CB_JOIN_REQUEST: Final = "JOIN_REQUEST"
152
-
153
-
154
- # USB generic device constants
155
- USB_AVAILABLE_ID: Final = "available"
156
-
157
- ATTR_MAC_ADDRESS: Final = "mac"
158
-
159
- SERVICE_USB_DEVICE_ADD: Final = "device_add"
160
- SERVICE_USB_DEVICE_REMOVE: Final = "device_remove"
161
- SERVICE_USB_DEVICE_SCHEMA: Final = vol.Schema(
162
- {vol.Required(ATTR_MAC_ADDRESS): cv.string}
163
- ) # pw-beta usb
164
-
165
-
166
- # USB Relay device constants
167
- USB_RELAY_ID: Final = "relay"
168
-
169
-
170
- # USB SED (battery powered) device constants
171
- ATTR_SED_STAY_ACTIVE: Final = "stay_active"
172
- ATTR_SED_SLEEP_FOR: Final = "sleep_for"
173
- ATTR_SED_MAINTENANCE_INTERVAL: Final = "maintenance_interval"
174
- ATTR_SED_CLOCK_SYNC: Final = "clock_sync"
175
- ATTR_SED_CLOCK_INTERVAL: Final = "clock_interval"
176
-
177
- SERVICE_USB_SED_BATTERY_CONFIG: Final = "configure_battery_savings"
178
- SERVICE_USB_SED_BATTERY_CONFIG_SCHEMA: Final = {
179
- vol.Required(ATTR_SED_STAY_ACTIVE): vol.All(
180
- vol.Coerce(int), vol.Range(min=1, max=120)
181
- ),
182
- vol.Required(ATTR_SED_SLEEP_FOR): vol.All(
183
- vol.Coerce(int), vol.Range(min=10, max=60)
184
- ),
185
- vol.Required(ATTR_SED_MAINTENANCE_INTERVAL): vol.All(
186
- vol.Coerce(int), vol.Range(min=5, max=1440)
187
- ),
188
- vol.Required(ATTR_SED_CLOCK_SYNC): cv.boolean,
189
- vol.Required(ATTR_SED_CLOCK_INTERVAL): vol.All(
190
- vol.Coerce(int), vol.Range(min=60, max=10080)
191
- ),
192
- }
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
- # USB Scan device constants
196
- USB_MOTION_ID: Final = "motion"
 
 
 
 
 
197
 
198
- ATTR_SCAN_DAYLIGHT_MODE: Final = "day_light"
199
- ATTR_SCAN_SENSITIVITY_MODE: Final = "sensitivity_mode"
200
- ATTR_SCAN_RESET_TIMER: Final = "reset_timer"
201
-
202
- SCAN_SENSITIVITY_HIGH: Final = "high"
203
- SCAN_SENSITIVITY_MEDIUM: Final = "medium"
204
- SCAN_SENSITIVITY_OFF: Final = "off"
205
- SCAN_SENSITIVITY_MODES = [
206
- SCAN_SENSITIVITY_HIGH,
207
- SCAN_SENSITIVITY_MEDIUM,
208
- SCAN_SENSITIVITY_OFF,
209
  ]
210
 
211
- SERVICE_USB_SCAN_CONFIG: Final = "configure_scan"
212
- SERVICE_USB_SCAN_CONFIG_SCHEMA = (
213
- {
214
- vol.Required(ATTR_SCAN_SENSITIVITY_MODE): vol.In(SCAN_SENSITIVITY_MODES),
215
- vol.Required(ATTR_SCAN_RESET_TIMER): vol.All(
216
- vol.Coerce(int), vol.Range(min=1, max=240)
217
- ),
218
- vol.Required(ATTR_SCAN_DAYLIGHT_MODE): cv.boolean,
219
- },
220
- )
 
1
  """Constants for Plugwise component."""
2
+
3
  from datetime import timedelta
4
  import logging
5
+ from typing import Final, Literal
 
 
6
 
7
  from homeassistant.const import Platform
 
8
 
9
  DOMAIN: Final = "plugwise"
10
 
11
  LOGGER = logging.getLogger(__package__)
12
 
13
+ ANNA_WITH_ADAM: Final = "anna_with_adam"
14
  API: Final = "api"
15
+ AVAILABLE: Final = "available"
16
+ DEV_CLASS: Final = "dev_class"
17
+ FLOW_SMILE: Final = "smile (Adam/Anna/P1)"
18
+ FLOW_STRETCH: Final = "stretch (Stretch)"
19
+ FLOW_TYPE: Final = "flow_type"
20
  GATEWAY: Final = "gateway"
21
+ LOCATION: Final = "location"
22
  PW_TYPE: Final = "plugwise_type"
23
+ REBOOT: Final = "reboot"
24
  SMILE: Final = "smile"
25
+ SMILE_OPEN_THERM: Final = "smile_open_therm"
26
+ SMILE_THERMO: Final = "smile_thermo"
27
  STRETCH: Final = "stretch"
28
  STRETCH_USERNAME: Final = "stretch"
29
+ UNKNOWN_SMILE: Final = "Unknown Smile"
 
 
 
 
 
 
30
 
31
+ PLATFORMS: Final[list[str]] = [
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  Platform.BINARY_SENSOR,
33
+ Platform.BUTTON,
34
  Platform.CLIMATE,
35
  Platform.NUMBER,
36
  Platform.SELECT,
37
  Platform.SENSOR,
38
  Platform.SWITCH,
39
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  ZEROCONF_MAP: Final[dict[str, str]] = {
41
  "smile": "Smile P1",
42
  "smile_thermo": "Smile Anna",
 
44
  "stretch": "Stretch",
45
  }
46
 
47
+ type NumberType = Literal[
48
+ "maximum_boiler_temperature",
49
+ "max_dhw_temperature",
50
+ "temperature_offset",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ type SelectType = Literal[
54
+ "select_dhw_mode",
55
+ "select_gateway_mode",
56
+ "select_regulation_mode",
57
+ "select_schedule",
58
+ "select_zone_profile",
59
+ ]
60
+ type SelectOptionsType = Literal[
61
+ "available_schedules",
62
+ "dhw_modes",
63
+ "gateway_modes",
64
+ "regulation_modes",
65
+ "zone_profiles",
66
+ ]
67
 
68
+ # Default directives
69
+ DEFAULT_MAX_TEMP: Final = 30
70
+ DEFAULT_MIN_TEMP: Final = 4
71
+ DEFAULT_PORT: Final = 80
72
+ DEFAULT_UPDATE_INTERVAL = timedelta(seconds=60)
73
+ DEFAULT_USERNAME: Final = "smile"
74
+ P1_UPDATE_INTERVAL = timedelta(seconds=10)
75
 
76
+ MASTER_THERMOSTATS: Final[list[str]] = [
77
+ "thermostat",
78
+ "thermostatic_radiator_valve",
79
+ "zone_thermometer",
80
+ "zone_thermostat",
 
 
 
 
 
 
81
  ]
82
 
83
+ # Select constants
84
+ SELECT_DHW_MODE: Final = "select_dhw_mode"
85
+ SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
86
+ SELECT_REGULATION_MODE: Final = "select_regulation_mode"
87
+ SELECT_SCHEDULE: Final = "select_schedule"
88
+ SELECT_ZONE_PROFILE: Final = "select_zone_profile"
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py RENAMED
@@ -1,110 +1,211 @@
1
  """DataUpdateCoordinator for Plugwise."""
2
- from datetime import timedelta
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  from homeassistant.config_entries import ConfigEntry
5
  from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
6
- from homeassistant.const import CONF_SCAN_INTERVAL # pw-beta options
7
  from homeassistant.core import HomeAssistant
8
  from homeassistant.exceptions import ConfigEntryError
 
9
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
10
  from homeassistant.helpers.debounce import Debouncer
11
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
12
- from plugwise import PlugwiseData, Smile
13
- from plugwise.exceptions import (
14
- ConnectionFailedError,
15
- InvalidAuthentication,
16
- InvalidXMLError,
17
- ResponseError,
18
- UnsupportedDeviceError,
 
19
  )
20
 
21
- from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_USERNAME, DOMAIN, LOGGER
22
 
23
 
24
- class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
25
  """Class to manage fetching Plugwise data from single endpoint."""
26
 
27
- _connected: bool = False
28
 
29
- def __init__(
30
- self,
31
- hass: HomeAssistant,
32
- entry: ConfigEntry,
33
- cooldown: float,
34
- update_interval: timedelta = timedelta(seconds=60),
35
- ) -> None: # pw-beta cooldown
36
  """Initialize the coordinator."""
37
  super().__init__(
38
  hass,
39
  LOGGER,
 
40
  name=DOMAIN,
41
- # Core directly updates from const's DEFAULT_SCAN_INTERVAL
42
- update_interval=update_interval,
43
  # Don't refresh immediately, give the device time to process
44
  # the change in state before we query it.
45
  request_refresh_debouncer=Debouncer(
46
  hass,
47
  LOGGER,
48
- cooldown=cooldown,
49
  immediate=False,
50
  ),
51
  )
52
 
53
  self.api = Smile(
54
- host=entry.data[CONF_HOST],
55
- username=entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
56
- password=entry.data[CONF_PASSWORD],
57
- port=entry.data.get(CONF_PORT, DEFAULT_PORT),
58
- timeout=30,
59
  websession=async_get_clientsession(hass, verify_ssl=False),
60
  )
61
- self._entry = entry
62
- self._unavailable_logged = False
63
- self.update_interval = update_interval
 
 
64
 
65
  async def _connect(self) -> None:
66
- """Connect to the Plugwise Smile."""
67
- self._connected = await self.api.connect()
68
- self.api.get_all_devices()
69
-
70
- self.update_interval = DEFAULT_SCAN_INTERVAL.get(
71
- self.api.smile_type, timedelta(seconds=60)
72
- ) # pw-beta options scan-interval
73
- if (custom_time := self._entry.options.get(CONF_SCAN_INTERVAL)) is not None:
74
- self.update_interval = timedelta(
75
- seconds=int(custom_time)
76
- ) # pragma: no cover # pw-beta options
77
 
78
- LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- async def _async_update_data(self) -> PlugwiseData:
 
81
  """Fetch data from Plugwise."""
82
- data = PlugwiseData(gateway={}, devices={})
83
-
84
  try:
85
  if not self._connected:
86
  await self._connect()
87
  data = await self.api.async_update()
88
- LOGGER.debug(f"{self.api.smile_name} data: %s", data)
89
- if self._unavailable_logged:
90
- self._unavailable_logged = False
 
 
91
  except InvalidAuthentication as err:
92
- if not self._unavailable_logged: # pw-beta add to Core
93
- self._unavailable_logged = True
94
- raise ConfigEntryError("Authentication failed") from err
 
 
 
 
 
 
95
  except (InvalidXMLError, ResponseError) as err:
96
- if not self._unavailable_logged: # pw-beta add to Core
97
- self._unavailable_logged = True
98
- raise UpdateFailed(
99
- "Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch"
100
- ) from err
 
 
 
 
101
  except UnsupportedDeviceError as err:
102
- if not self._unavailable_logged: # pw-beta add to Core
103
- self._unavailable_logged = True
104
- raise ConfigEntryError("Device with unsupported firmware") from err
105
- except ConnectionFailedError as err:
106
- if not self._unavailable_logged: # pw-beta add to Core
107
- self._unavailable_logged = True
108
- raise UpdateFailed("Failed to connect") from err
109
 
 
 
110
  return data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """DataUpdateCoordinator for Plugwise."""
2
+
3
+ from typing import override
4
+
5
+ from packaging.version import Version
6
+ from plugwise import GwEntityData, Smile
7
+ from plugwise.exceptions import (
8
+ ConnectionFailedError,
9
+ InvalidAuthentication,
10
+ InvalidSetupError,
11
+ InvalidXMLError,
12
+ PlugwiseError,
13
+ ResponseError,
14
+ UnsupportedDeviceError,
15
+ )
16
 
17
  from homeassistant.config_entries import ConfigEntry
18
  from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
 
19
  from homeassistant.core import HomeAssistant
20
  from homeassistant.exceptions import ConfigEntryError
21
+ from homeassistant.helpers import device_registry as dr
22
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
23
  from homeassistant.helpers.debounce import Debouncer
24
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
25
+
26
+ from .const import (
27
+ DEFAULT_PORT,
28
+ DEFAULT_UPDATE_INTERVAL,
29
+ DEFAULT_USERNAME,
30
+ DOMAIN,
31
+ LOGGER,
32
+ P1_UPDATE_INTERVAL,
33
  )
34
 
35
+ type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
36
 
37
 
38
+ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData]]):
39
  """Class to manage fetching Plugwise data from single endpoint."""
40
 
41
+ config_entry: PlugwiseConfigEntry
42
 
43
+ def __init__(self, hass: HomeAssistant, config_entry: PlugwiseConfigEntry) -> None:
 
 
 
 
 
 
44
  """Initialize the coordinator."""
45
  super().__init__(
46
  hass,
47
  LOGGER,
48
+ config_entry=config_entry,
49
  name=DOMAIN,
50
+ update_interval=DEFAULT_UPDATE_INTERVAL,
 
51
  # Don't refresh immediately, give the device time to process
52
  # the change in state before we query it.
53
  request_refresh_debouncer=Debouncer(
54
  hass,
55
  LOGGER,
56
+ cooldown=1.5,
57
  immediate=False,
58
  ),
59
  )
60
 
61
  self.api = Smile(
62
+ host=self.config_entry.data[CONF_HOST],
63
+ username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
64
+ password=self.config_entry.data[CONF_PASSWORD],
65
+ port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
 
66
  websession=async_get_clientsession(hass, verify_ssl=False),
67
  )
68
+ self._connected: bool = False
69
+ self._current_devices: set[str] = set()
70
+ self._firmware_list: dict[str, str | None] = {}
71
+ self._stored_devices: set[str] = set()
72
+ self.new_devices: set[str] = set()
73
 
74
  async def _connect(self) -> None:
75
+ """Connect to the Plugwise Smile.
 
 
 
 
 
 
 
 
 
 
76
 
77
+ A Version object is received when the connection succeeds.
78
+ """
79
+ version = await self.api.connect()
80
+ self._connected = isinstance(version, Version)
81
+ if self._connected and self.api.smile.type == "power":
82
+ self.update_interval = P1_UPDATE_INTERVAL
83
+
84
+ @override
85
+ async def _async_setup(self) -> None:
86
+ """Initialize the update_data process."""
87
+ device_reg = dr.async_get(self.hass)
88
+ device_entries = dr.async_entries_for_config_entry(
89
+ device_reg, self.config_entry.entry_id
90
+ )
91
+ self._stored_devices = {
92
+ identifier[1]
93
+ for device_entry in device_entries
94
+ for identifier in device_entry.identifiers
95
+ if identifier[0] == DOMAIN
96
+ }
97
 
98
+ @override
99
+ async def _async_update_data(self) -> dict[str, GwEntityData]:
100
  """Fetch data from Plugwise."""
 
 
101
  try:
102
  if not self._connected:
103
  await self._connect()
104
  data = await self.api.async_update()
105
+ except ConnectionFailedError as err:
106
+ raise UpdateFailed(
107
+ translation_domain=DOMAIN,
108
+ translation_key="failed_to_connect",
109
+ ) from err
110
  except InvalidAuthentication as err:
111
+ raise ConfigEntryError(
112
+ translation_domain=DOMAIN,
113
+ translation_key="authentication_failed",
114
+ ) from err
115
+ except InvalidSetupError as err:
116
+ raise ConfigEntryError(
117
+ translation_domain=DOMAIN,
118
+ translation_key="invalid_setup",
119
+ ) from err
120
  except (InvalidXMLError, ResponseError) as err:
121
+ raise UpdateFailed(
122
+ translation_domain=DOMAIN,
123
+ translation_key="response_error",
124
+ ) from err
125
+ except PlugwiseError as err:
126
+ raise UpdateFailed(
127
+ translation_domain=DOMAIN,
128
+ translation_key="data_incomplete_or_missing",
129
+ ) from err
130
  except UnsupportedDeviceError as err:
131
+ raise ConfigEntryError(
132
+ translation_domain=DOMAIN,
133
+ translation_key="unsupported_firmware",
134
+ ) from err
 
 
 
135
 
136
+ self._add_remove_devices(data)
137
+ self._update_device_firmware(data)
138
  return data
139
+
140
+ def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
141
+ """Add new Plugwise devices, remove non-existing devices."""
142
+ set_of_data = set(data)
143
+ # Check for new or removed devices,
144
+ # 'new_devices' contains all devices present in 'data'
145
+ # at init ('self._current_devices' is empty) this is
146
+ # required for the proper initialization of all the
147
+ # present platform entities.
148
+ self.new_devices = set_of_data - self._current_devices
149
+ for device_id in self.new_devices:
150
+ self._firmware_list.setdefault(device_id, data[device_id].get("firmware"))
151
+
152
+ current_devices = (
153
+ self._stored_devices if not self._current_devices else self._current_devices
154
+ )
155
+ self._current_devices = set_of_data
156
+ if removed_devices := (current_devices - set_of_data): # device(s) to remove
157
+ self._remove_devices(removed_devices)
158
+
159
+ def _remove_devices(self, removed_devices: set[str]) -> None:
160
+ """Clean registries when removed devices found."""
161
+ device_reg = dr.async_get(self.hass)
162
+ for device_id in removed_devices:
163
+ if (
164
+ device_entry := device_reg.async_get_device({(DOMAIN, device_id)})
165
+ ) is not None:
166
+ device_reg.async_update_device(
167
+ device_entry.id, remove_config_entry_id=self.config_entry.entry_id
168
+ )
169
+ LOGGER.debug(
170
+ "%s %s %s removed from device_registry",
171
+ DOMAIN,
172
+ device_entry.model,
173
+ device_id,
174
+ )
175
+
176
+ self._firmware_list.pop(device_id, None)
177
+
178
+ def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
179
+ """Detect firmware changes and update the device registry."""
180
+ for device_id, device in data.items():
181
+ # Only update firmware when the key is present and not None, to avoid
182
+ # wiping stored firmware on partial or transient updates.
183
+ if "firmware" not in device:
184
+ continue
185
+ new_firmware = device.get("firmware")
186
+ if new_firmware is None:
187
+ continue
188
+ if (
189
+ device_id in self._firmware_list
190
+ and new_firmware != self._firmware_list[device_id]
191
+ ):
192
+ updated = self._update_firmware_in_dr(device_id, new_firmware)
193
+ if updated:
194
+ self._firmware_list[device_id] = new_firmware
195
+
196
+ def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> bool:
197
+ """Update device sw_version in device_registry."""
198
+ device_reg = dr.async_get(self.hass)
199
+ if (
200
+ device_entry := device_reg.async_get_device({(DOMAIN, device_id)})
201
+ ) is not None:
202
+ device_reg.async_update_device(device_entry.id, sw_version=firmware)
203
+ LOGGER.debug(
204
+ "Firmware in device_registry updated for %s %s %s",
205
+ DOMAIN,
206
+ device_entry.model,
207
+ device_id,
208
+ )
209
+ return True
210
+
211
+ return False # pragma: no cover
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/diagnostics.py RENAMED
@@ -1,24 +1,15 @@
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 .const import COORDINATOR # pw-beta
10
- from .const import DOMAIN
11
- from .coordinator import PlugwiseDataUpdateCoordinator
12
 
13
 
14
  async def async_get_config_entry_diagnostics(
15
- hass: HomeAssistant, entry: ConfigEntry
16
  ) -> dict[str, Any]:
17
  """Return diagnostics for a config entry."""
18
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
19
- COORDINATOR
20
- ]
21
- return {
22
- "gateway": coordinator.data.gateway,
23
- "devices": coordinator.data.devices,
24
- }
 
1
  """Diagnostics support for Plugwise."""
 
2
 
3
  from typing import Any
4
 
 
5
  from homeassistant.core import HomeAssistant
6
 
7
+ from .coordinator import PlugwiseConfigEntry
 
 
8
 
9
 
10
  async def async_get_config_entry_diagnostics(
11
+ hass: HomeAssistant, entry: PlugwiseConfigEntry
12
  ) -> dict[str, Any]:
13
  """Return diagnostics for a config entry."""
14
+ coordinator = entry.runtime_data
15
+ return coordinator.data
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py RENAMED
@@ -1,16 +1,18 @@
1
  """Generic Plugwise Entity Class."""
2
- from __future__ import annotations
 
 
 
3
 
4
  from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
5
  from homeassistant.helpers.device_registry import (
6
  CONNECTION_NETWORK_MAC,
7
  CONNECTION_ZIGBEE,
 
8
  )
9
- from homeassistant.helpers.entity import DeviceInfo
10
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
11
- from plugwise.constants import DeviceData
12
 
13
- from .const import DOMAIN
14
  from .coordinator import PlugwiseDataUpdateCoordinator
15
 
16
 
@@ -28,54 +30,57 @@
28
  super().__init__(coordinator)
29
  self._dev_id = device_id
30
 
31
- configuration_url: str | None = None
32
- if entry := self.coordinator.config_entry:
33
- configuration_url = f"http://{entry.data[CONF_HOST]}"
 
 
 
 
 
 
 
34
 
35
- data = coordinator.data.devices[device_id]
36
  connections = set()
37
- if mac := data.get("mac_address"):
38
  connections.add((CONNECTION_NETWORK_MAC, mac))
39
- if mac := data.get("zigbee_mac_address"):
40
- connections.add((CONNECTION_ZIGBEE, mac))
41
 
 
42
  self._attr_device_info = DeviceInfo(
43
  configuration_url=configuration_url,
44
  identifiers={(DOMAIN, device_id)},
45
  connections=connections,
46
- manufacturer=data.get("vendor"),
47
- model=data.get("model"),
48
- name=coordinator.data.gateway["smile_name"],
49
- sw_version=data.get("firmware"),
50
- hw_version=data.get("hardware"),
 
51
  )
52
 
53
- if device_id != coordinator.data.gateway["gateway_id"]:
 
54
  self._attr_device_info.update(
55
  {
56
- ATTR_NAME: data.get("name"),
57
- ATTR_VIA_DEVICE: (
58
- DOMAIN,
59
- str(self.coordinator.data.gateway["gateway_id"]),
60
- ),
61
  }
62
  )
63
 
64
  @property
 
65
  def available(self) -> bool:
66
  """Return if entity is available."""
67
  return (
68
- self._dev_id in self.coordinator.data.devices
69
- and ("available" not in self.device or self.device["available"])
70
  and super().available
71
  )
72
 
73
  @property
74
- def device(self) -> DeviceData:
75
  """Return data for this device."""
76
- return self.coordinator.data.devices[self._dev_id]
77
-
78
- async def async_added_to_hass(self) -> None:
79
- """Subscribe to updates."""
80
- self._handle_coordinator_update()
81
- await super().async_added_to_hass()
 
1
  """Generic Plugwise Entity Class."""
2
+
3
+ from typing import override
4
+
5
+ from plugwise import GwEntityData
6
 
7
  from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
8
  from homeassistant.helpers.device_registry import (
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
 
 
30
  super().__init__(coordinator)
31
  self._dev_id = device_id
32
 
33
+ api = coordinator.api
34
+ gateway_id = api.gateway_id
35
+ entry = coordinator.config_entry
36
+
37
+ # Link configuration-URL for the gateway device
38
+ configuration_url = (
39
+ f"http://{entry.data[CONF_HOST]}"
40
+ if device_id == gateway_id and entry
41
+ else None
42
+ )
43
 
44
+ # Build connections set
45
  connections = set()
46
+ if mac := self.device.get("mac_address"):
47
  connections.add((CONNECTION_NETWORK_MAC, mac))
48
+ if zigbee_mac := self.device.get("zigbee_mac_address"):
49
+ connections.add((CONNECTION_ZIGBEE, zigbee_mac))
50
 
51
+ # Set base device info
52
  self._attr_device_info = DeviceInfo(
53
  configuration_url=configuration_url,
54
  identifiers={(DOMAIN, device_id)},
55
  connections=connections,
56
+ manufacturer=self.device.get("vendor"),
57
+ model=self.device.get("model"),
58
+ model_id=self.device.get("model_id"),
59
+ name=api.smile.name,
60
+ sw_version=self.device.get("firmware"),
61
+ hw_version=self.device.get("hardware"),
62
  )
63
 
64
+ # Add extra info if not the gateway device
65
+ if device_id != gateway_id:
66
  self._attr_device_info.update(
67
  {
68
+ ATTR_NAME: self.device.get(ATTR_NAME),
69
+ ATTR_VIA_DEVICE: (DOMAIN, gateway_id),
 
 
 
70
  }
71
  )
72
 
73
  @property
74
+ @override
75
  def available(self) -> bool:
76
  """Return if entity is available."""
77
  return (
78
+ self._dev_id in self.coordinator.data
79
+ and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
80
  and super().available
81
  )
82
 
83
  @property
84
+ def device(self) -> GwEntityData:
85
  """Return data for this device."""
86
+ return self.coordinator.data[self._dev_id]
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json RENAMED
@@ -1,13 +1,13 @@
1
  {
2
  "domain": "plugwise",
3
- "name": "Plugwise Beta",
4
- "after_dependencies": ["usb", "zeroconf"],
5
- "codeowners": ["@CoMPaTech", "@bouwew", "@brefra"],
6
  "config_flow": true,
7
- "documentation": "https://github.com/plugwise/plugwise-beta",
8
  "integration_type": "hub",
9
  "iot_class": "local_polling",
10
  "loggers": ["plugwise"],
11
- "requirements": ["https://test-files.pythonhosted.org/packages/e0/8a/b52a039f503d6d076c6757c059bd6e9dba6cdd8c6a14dab5e4e8a64e07ef/plugwise-0.31.5a4.tar.gz#plugwise==0.31.5a4"],
12
- "version": "0.34.11a3"
 
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.11.4"],
12
+ "zeroconf": ["_plugwise._tcp.local."]
13
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py RENAMED
@@ -1,8 +1,7 @@
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 homeassistant.components.number import (
8
  NumberDeviceClass,
@@ -10,86 +9,73 @@
10
  NumberEntityDescription,
11
  NumberMode,
12
  )
13
- from homeassistant.config_entries import ConfigEntry
14
- from homeassistant.const import TEMP_CELSIUS, EntityCategory
15
- from homeassistant.core import HomeAssistant
16
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
17
- from plugwise import Smile
18
-
19
- from .const import COORDINATOR # pw-beta
20
- from .const import DOMAIN, LOGGER
21
- from .coordinator import PlugwiseDataUpdateCoordinator
22
- from .entity import PlugwiseEntity
23
-
24
 
25
- @dataclass
26
- class PlugwiseEntityDescriptionMixin:
27
- """Mixin values for Plugwse entities."""
 
28
 
29
- command: Callable[[Smile, str, float], Awaitable[None]]
30
 
31
 
32
- @dataclass
33
- class PlugwiseNumberEntityDescription(
34
- NumberEntityDescription, PlugwiseEntityDescriptionMixin
35
- ):
36
  """Class describing Plugwise Number entities."""
37
 
38
- native_max_value_key: str | None = None
39
- native_min_value_key: str | None = None
40
- native_step_key: str | None = None
41
- native_value_key: str | None = None
42
 
43
 
44
  NUMBER_TYPES = (
45
  PlugwiseNumberEntityDescription(
46
  key="maximum_boiler_temperature",
47
- command=lambda api, number, value: api.set_number_setpoint(number, value),
48
  device_class=NumberDeviceClass.TEMPERATURE,
49
- name="Maximum boiler temperature setpoint",
50
  entity_category=EntityCategory.CONFIG,
51
- native_max_value_key="upper_bound",
52
- native_min_value_key="lower_bound",
53
- native_step_key="resolution",
54
- native_unit_of_measurement=TEMP_CELSIUS,
55
- native_value_key="setpoint",
56
  ),
57
  PlugwiseNumberEntityDescription(
58
  key="max_dhw_temperature",
59
- command=lambda api, number, value: api.set_number_setpoint(number, value),
 
 
 
 
 
 
 
60
  device_class=NumberDeviceClass.TEMPERATURE,
61
- name="Domestic hot water setpoint",
62
  entity_category=EntityCategory.CONFIG,
63
- native_max_value_key="upper_bound",
64
- native_min_value_key="lower_bound",
65
- native_step_key="resolution",
66
- native_unit_of_measurement=TEMP_CELSIUS,
67
- native_value_key="setpoint",
68
  ),
69
  )
70
 
71
 
72
  async def async_setup_entry(
73
  hass: HomeAssistant,
74
- config_entry: ConfigEntry,
75
- async_add_entities: AddEntitiesCallback,
76
  ) -> None:
77
  """Set up Plugwise number platform."""
 
78
 
79
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][
80
- config_entry.entry_id
81
- ][COORDINATOR]
82
-
83
- entities: list[PlugwiseNumberEntity] = []
84
- for device_id, device in coordinator.data.devices.items():
85
- for description in NUMBER_TYPES:
86
- if description.key in device and "setpoint" in device[description.key]:
87
- entities.append(
88
- PlugwiseNumberEntity(coordinator, device_id, description)
89
- )
90
- LOGGER.debug("Add %s %s number", device["name"], description.name)
91
 
92
- async_add_entities(entities)
 
93
 
94
 
95
  class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
@@ -105,47 +91,28 @@
105
  ) -> None:
106
  """Initiate Plugwise Number."""
107
  super().__init__(coordinator, device_id)
108
- self.entity_description = description
109
- self._attr_unique_id = f"{device_id}-{description.key}"
110
  self._attr_mode = NumberMode.BOX
 
 
 
 
 
111
 
112
- @property
113
- def native_step(self) -> float:
114
- """Return the setpoint step value."""
115
- return max(
116
- self.device[self.entity_description.key][
117
- self.entity_description.native_step_key
118
- ],
119
- 1,
120
- )
121
 
122
  @property
 
123
  def native_value(self) -> float:
124
  """Return the present setpoint value."""
125
- return self.device[self.entity_description.key][
126
- self.entity_description.native_value_key
127
- ]
128
-
129
- @property
130
- def native_min_value(self) -> float:
131
- """Return the setpoint min. value."""
132
- return self.device[self.entity_description.key][
133
- self.entity_description.native_min_value_key
134
- ]
135
-
136
- @property
137
- def native_max_value(self) -> float:
138
- """Return the setpoint max. value."""
139
- return self.device[self.entity_description.key][
140
- self.entity_description.native_max_value_key
141
- ]
142
 
 
 
143
  async def async_set_native_value(self, value: float) -> None:
144
  """Change to the new setpoint value."""
145
- await self.entity_description.command(
146
- self.coordinator.api, self.entity_description.key, value
147
- )
148
- LOGGER.debug(
149
- "Setting %s to %s was successful", self.entity_description.name, value
150
  )
151
- await self.coordinator.async_request_refresh()
 
1
  """Number platform for Plugwise integration."""
 
2
 
 
3
  from dataclasses import dataclass
4
+ from typing import override
5
 
6
  from homeassistant.components.number import (
7
  NumberDeviceClass,
 
9
  NumberEntityDescription,
10
  NumberMode,
11
  )
12
+ from homeassistant.const import EntityCategory, UnitOfTemperature
13
+ from homeassistant.core import HomeAssistant, callback
14
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
 
 
 
 
 
 
 
 
15
 
16
+ from .const import NumberType
17
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
18
+ from .entity import PlugwiseEntity
19
+ from .util import plugwise_command
20
 
21
+ PARALLEL_UPDATES = 0
22
 
23
 
24
+ @dataclass(frozen=True, kw_only=True)
25
+ class PlugwiseNumberEntityDescription(NumberEntityDescription):
 
 
26
  """Class describing Plugwise Number entities."""
27
 
28
+ key: NumberType
 
 
 
29
 
30
 
31
  NUMBER_TYPES = (
32
  PlugwiseNumberEntityDescription(
33
  key="maximum_boiler_temperature",
34
+ translation_key="maximum_boiler_temperature",
35
  device_class=NumberDeviceClass.TEMPERATURE,
 
36
  entity_category=EntityCategory.CONFIG,
37
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 
 
 
 
38
  ),
39
  PlugwiseNumberEntityDescription(
40
  key="max_dhw_temperature",
41
+ translation_key="max_dhw_temperature",
42
+ device_class=NumberDeviceClass.TEMPERATURE,
43
+ entity_category=EntityCategory.CONFIG,
44
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
45
+ ),
46
+ PlugwiseNumberEntityDescription(
47
+ key="temperature_offset",
48
+ translation_key="temperature_offset",
49
  device_class=NumberDeviceClass.TEMPERATURE,
 
50
  entity_category=EntityCategory.CONFIG,
51
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 
 
 
 
52
  ),
53
  )
54
 
55
 
56
  async def async_setup_entry(
57
  hass: HomeAssistant,
58
+ entry: PlugwiseConfigEntry,
59
+ async_add_entities: AddConfigEntryEntitiesCallback,
60
  ) -> None:
61
  """Set up Plugwise number platform."""
62
+ coordinator = entry.runtime_data
63
 
64
+ @callback
65
+ def _add_entities() -> None:
66
+ """Add Entities."""
67
+ if not coordinator.new_devices:
68
+ return
69
+
70
+ async_add_entities(
71
+ PlugwiseNumberEntity(coordinator, device_id, description)
72
+ for device_id in coordinator.new_devices
73
+ for description in NUMBER_TYPES
74
+ if description.key in coordinator.data[device_id]
75
+ )
76
 
77
+ _add_entities()
78
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
79
 
80
 
81
  class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
 
91
  ) -> None:
92
  """Initiate Plugwise Number."""
93
  super().__init__(coordinator, device_id)
 
 
94
  self._attr_mode = NumberMode.BOX
95
+ self._attr_native_max_value = self.device[description.key]["upper_bound"]
96
+ self._attr_native_min_value = self.device[description.key]["lower_bound"]
97
+ self._attr_unique_id = f"{device_id}-{description.key}"
98
+ self.device_id = device_id
99
+ self.entity_description = description
100
 
101
+ native_step = self.device[description.key]["resolution"]
102
+ if description.key != "temperature_offset":
103
+ native_step = max(native_step, 0.5)
104
+ self._attr_native_step = native_step
 
 
 
 
 
105
 
106
  @property
107
+ @override
108
  def native_value(self) -> float:
109
  """Return the present setpoint value."""
110
+ return self.device[self.entity_description.key]["setpoint"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
+ @plugwise_command
113
+ @override
114
  async def async_set_native_value(self, value: float) -> None:
115
  """Change to the new setpoint value."""
116
+ await self.coordinator.api.set_number(
117
+ self.device_id, self.entity_description.key, value
 
 
 
118
  )
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py RENAMED
@@ -1,96 +1,93 @@
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
- from typing import Any
7
 
8
  from homeassistant.components.select import SelectEntity, SelectEntityDescription
9
- from homeassistant.config_entries import ConfigEntry
10
  from homeassistant.const import STATE_ON, EntityCategory
11
- from homeassistant.core import HomeAssistant
12
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
13
- from plugwise import Smile
14
-
15
- from .const import COORDINATOR # pw-beta
16
- from .const import DOMAIN, LOGGER
17
- from .coordinator import PlugwiseDataUpdateCoordinator
 
 
 
 
 
 
18
  from .entity import PlugwiseEntity
 
19
 
20
  PARALLEL_UPDATES = 0
21
 
22
 
23
- @dataclass
24
- class PlugwiseSelectDescriptionMixin:
25
- """Mixin values for Plugwise Select entities."""
26
-
27
- command: Callable[[Smile, str, str], Awaitable[Any]]
28
- current_option_key: str
29
- options_key: str
30
 
31
-
32
- @dataclass
33
- class PlugwiseSelectEntityDescription(
34
- SelectEntityDescription, PlugwiseSelectDescriptionMixin
35
- ):
36
- """Class describing Plugwise Number entities."""
37
 
38
 
39
  SELECT_TYPES = (
40
  PlugwiseSelectEntityDescription(
41
- key="select_schedule",
42
- name="Thermostat schedule",
43
- icon="mdi:calendar-clock",
44
- command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON),
45
- current_option_key="selected_schedule",
46
  options_key="available_schedules",
47
  ),
48
  PlugwiseSelectEntityDescription(
49
- key="select_regulation_mode",
50
- name="Regulation mode",
51
- icon="mdi:hvac",
52
  entity_category=EntityCategory.CONFIG,
53
- translation_key="regulation_mode",
54
- command=lambda api, loc, opt: api.set_regulation_mode(opt),
55
- current_option_key="regulation_mode",
56
  options_key="regulation_modes",
57
  ),
58
  PlugwiseSelectEntityDescription(
59
- key="select_dhw_mode",
60
- name="DHW mode",
61
- icon="mdi:shower",
62
  entity_category=EntityCategory.CONFIG,
63
- translation_key="dhw_mode",
64
- command=lambda api, loc, opt: api.set_dhw_mode(opt),
65
- current_option_key="dhw_mode",
66
  options_key="dhw_modes",
67
  ),
 
 
 
 
 
 
 
 
 
 
 
 
68
  )
69
 
70
 
71
  async def async_setup_entry(
72
  hass: HomeAssistant,
73
- config_entry: ConfigEntry,
74
- async_add_entities: AddEntitiesCallback,
75
  ) -> None:
76
  """Set up the Smile selector from a config entry."""
77
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][
78
- config_entry.entry_id
79
- ][COORDINATOR]
80
-
81
- entities: list[PlugwiseSelectEntity] = []
82
- for device_id, device in coordinator.data.devices.items():
83
- for description in SELECT_TYPES:
84
- if (
85
- description.options_key in device
86
- and len(device[description.options_key]) > 1
87
- ):
88
- entities.append(
89
- PlugwiseSelectEntity(coordinator, device_id, description)
90
- )
91
- LOGGER.debug("Add %s %s selector", device["name"], description.name)
92
 
93
- async_add_entities(entities)
 
94
 
95
 
96
  class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
@@ -106,27 +103,32 @@
106
  ) -> None:
107
  """Initialise the selector."""
108
  super().__init__(coordinator, device_id)
109
- self.entity_description = entity_description
110
  self._attr_unique_id = f"{device_id}-{entity_description.key}"
 
 
 
 
 
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.current_option_key]
116
 
117
  @property
 
118
  def options(self) -> list[str]:
119
- """Return the selectable entity options."""
120
  return self.device[self.entity_description.options_key]
121
 
 
 
122
  async def async_select_option(self, option: str) -> None:
123
- """Change to the selected entity option."""
124
- await self.entity_description.command(
125
- self.coordinator.api, self.device["location"], option
126
- )
127
- LOGGER.debug(
128
- "Set %s to %s was successful.",
129
- self.entity_description.name,
130
- option,
131
  )
132
- await self.coordinator.async_request_refresh()
 
1
  """Plugwise Select component for Home Assistant."""
 
2
 
 
3
  from dataclasses import dataclass
4
+ from typing import override
5
 
6
  from homeassistant.components.select import SelectEntity, SelectEntityDescription
 
7
  from homeassistant.const import STATE_ON, EntityCategory
8
+ from homeassistant.core import HomeAssistant, callback
9
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
10
+
11
+ from .const import (
12
+ SELECT_DHW_MODE,
13
+ SELECT_GATEWAY_MODE,
14
+ SELECT_REGULATION_MODE,
15
+ SELECT_SCHEDULE,
16
+ SELECT_ZONE_PROFILE,
17
+ SelectOptionsType,
18
+ SelectType,
19
+ )
20
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
21
  from .entity import PlugwiseEntity
22
+ from .util import plugwise_command
23
 
24
  PARALLEL_UPDATES = 0
25
 
26
 
27
+ @dataclass(frozen=True, kw_only=True)
28
+ class PlugwiseSelectEntityDescription(SelectEntityDescription):
29
+ """Class describing Plugwise Select entities."""
 
 
 
 
30
 
31
+ key: SelectType
32
+ options_key: SelectOptionsType
 
 
 
 
33
 
34
 
35
  SELECT_TYPES = (
36
  PlugwiseSelectEntityDescription(
37
+ key=SELECT_SCHEDULE,
38
+ translation_key=SELECT_SCHEDULE,
 
 
 
39
  options_key="available_schedules",
40
  ),
41
  PlugwiseSelectEntityDescription(
42
+ key=SELECT_REGULATION_MODE,
43
+ translation_key=SELECT_REGULATION_MODE,
 
44
  entity_category=EntityCategory.CONFIG,
 
 
 
45
  options_key="regulation_modes",
46
  ),
47
  PlugwiseSelectEntityDescription(
48
+ key=SELECT_DHW_MODE,
49
+ translation_key=SELECT_DHW_MODE,
 
50
  entity_category=EntityCategory.CONFIG,
 
 
 
51
  options_key="dhw_modes",
52
  ),
53
+ PlugwiseSelectEntityDescription(
54
+ key=SELECT_GATEWAY_MODE,
55
+ translation_key=SELECT_GATEWAY_MODE,
56
+ entity_category=EntityCategory.CONFIG,
57
+ options_key="gateway_modes",
58
+ ),
59
+ PlugwiseSelectEntityDescription(
60
+ key=SELECT_ZONE_PROFILE,
61
+ translation_key=SELECT_ZONE_PROFILE,
62
+ entity_category=EntityCategory.CONFIG,
63
+ options_key="zone_profiles",
64
+ ),
65
  )
66
 
67
 
68
  async def async_setup_entry(
69
  hass: HomeAssistant,
70
+ entry: PlugwiseConfigEntry,
71
+ async_add_entities: AddConfigEntryEntitiesCallback,
72
  ) -> None:
73
  """Set up the Smile selector from a config entry."""
74
+ coordinator = entry.runtime_data
75
+
76
+ @callback
77
+ def _add_entities() -> None:
78
+ """Add Entities."""
79
+ if not coordinator.new_devices:
80
+ return
81
+
82
+ async_add_entities(
83
+ PlugwiseSelectEntity(coordinator, device_id, description)
84
+ for device_id in coordinator.new_devices
85
+ for description in SELECT_TYPES
86
+ if coordinator.data[device_id].get(description.options_key)
87
+ )
 
88
 
89
+ _add_entities()
90
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
91
 
92
 
93
  class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
 
103
  ) -> None:
104
  """Initialise the selector."""
105
  super().__init__(coordinator, device_id)
 
106
  self._attr_unique_id = f"{device_id}-{entity_description.key}"
107
+ self.entity_description = entity_description
108
+
109
+ self._location = device_id
110
+ if (location := self.device.get("location")) is not None:
111
+ self._location = location
112
 
113
  @property
114
+ @override
115
+ def current_option(self) -> str | None:
116
  """Return the selected entity option to represent the entity state."""
117
+ return self.device[self.entity_description.key]
118
 
119
  @property
120
+ @override
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
+ @override
127
  async def async_select_option(self, option: str) -> None:
128
+ """Change to the selected entity option.
129
+
130
+ self._location and STATE_ON are required for the thermostat-schedule select.
131
+ """
132
+ await self.coordinator.api.set_select(
133
+ self.entity_description.key, self._location, option, STATE_ON
 
 
134
  )
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py RENAMED
@@ -1,52 +1,434 @@
1
  """Plugwise Sensor component for Home Assistant."""
2
- from __future__ import annotations
3
 
4
- from homeassistant.components.sensor import SensorEntity
5
- from homeassistant.config_entries import ConfigEntry
6
- from homeassistant.core import HomeAssistant
7
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
8
 
9
- from .const import COORDINATOR, DOMAIN, LOGGER
10
 
11
- from .coordinator import PlugwiseDataUpdateCoordinator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  from .entity import PlugwiseEntity
13
- from .models import PW_SENSOR_TYPES, PlugwiseSensorEntityDescription
14
 
 
15
  PARALLEL_UPDATES = 0
16
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  async def async_setup_entry(
19
  hass: HomeAssistant,
20
- config_entry: ConfigEntry,
21
- async_add_entities: AddEntitiesCallback,
22
  ) -> None:
23
  """Set up the Smile sensors from a config entry."""
24
- coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
25
 
26
- entities: list[PlugwiseSensorEntity] = []
27
- for device_id, device in coordinator.data.devices.items():
28
- for description in PW_SENSOR_TYPES:
29
- if (
30
- "sensors" not in device
31
- or device["sensors"].get(description.key) is None
32
- ):
33
- continue
34
-
35
- entities.append(
36
- PlugwiseSensorEntity(
37
- coordinator,
38
- device_id,
39
- description,
40
- )
41
- )
42
- LOGGER.debug("Add %s sensor", description.key)
43
 
44
- async_add_entities(entities)
 
45
 
46
 
47
  class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
48
  """Represent Plugwise Sensors."""
49
 
 
 
50
  def __init__(
51
  self,
52
  coordinator: PlugwiseDataUpdateCoordinator,
@@ -55,10 +437,11 @@
55
  ) -> None:
56
  """Initialise the sensor."""
57
  super().__init__(coordinator, device_id)
58
- self.entity_description = description
59
  self._attr_unique_id = f"{device_id}-{description.key}"
 
60
 
61
  @property
62
- def native_value(self) -> int | float | None:
 
63
  """Return the value reported by the sensor."""
64
- return self.device["sensors"].get(self.entity_description.key)
 
1
  """Plugwise Sensor component for Home Assistant."""
 
2
 
3
+ from dataclasses import dataclass
4
+ from typing import override
 
 
5
 
6
+ from plugwise.constants import SensorType
7
 
8
+ from homeassistant.components.sensor import (
9
+ SensorDeviceClass,
10
+ SensorEntity,
11
+ SensorEntityDescription,
12
+ SensorStateClass,
13
+ )
14
+ from homeassistant.const import (
15
+ LIGHT_LUX,
16
+ PERCENTAGE,
17
+ EntityCategory,
18
+ UnitOfElectricPotential,
19
+ UnitOfEnergy,
20
+ UnitOfPower,
21
+ UnitOfPressure,
22
+ UnitOfTemperature,
23
+ UnitOfVolume,
24
+ UnitOfVolumeFlowRate,
25
+ )
26
+ from homeassistant.core import HomeAssistant, callback
27
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
28
+
29
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
30
  from .entity import PlugwiseEntity
 
31
 
32
+ # Coordinator is used to centralize the data updates
33
  PARALLEL_UPDATES = 0
34
 
35
 
36
+ @dataclass(frozen=True)
37
+ class PlugwiseSensorEntityDescription(SensorEntityDescription):
38
+ """Describes Plugwise sensor entity."""
39
+
40
+ key: SensorType
41
+
42
+
43
+ SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
44
+ PlugwiseSensorEntityDescription(
45
+ key="setpoint",
46
+ translation_key="setpoint",
47
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
48
+ device_class=SensorDeviceClass.TEMPERATURE,
49
+ state_class=SensorStateClass.MEASUREMENT,
50
+ ),
51
+ PlugwiseSensorEntityDescription(
52
+ key="setpoint_high",
53
+ translation_key="cooling_setpoint",
54
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
55
+ device_class=SensorDeviceClass.TEMPERATURE,
56
+ state_class=SensorStateClass.MEASUREMENT,
57
+ ),
58
+ PlugwiseSensorEntityDescription(
59
+ key="setpoint_low",
60
+ translation_key="heating_setpoint",
61
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
62
+ device_class=SensorDeviceClass.TEMPERATURE,
63
+ state_class=SensorStateClass.MEASUREMENT,
64
+ ),
65
+ PlugwiseSensorEntityDescription(
66
+ key="temperature",
67
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
68
+ device_class=SensorDeviceClass.TEMPERATURE,
69
+ state_class=SensorStateClass.MEASUREMENT,
70
+ ),
71
+ PlugwiseSensorEntityDescription(
72
+ key="intended_boiler_temperature",
73
+ translation_key="intended_boiler_temperature",
74
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
75
+ device_class=SensorDeviceClass.TEMPERATURE,
76
+ entity_category=EntityCategory.DIAGNOSTIC,
77
+ state_class=SensorStateClass.MEASUREMENT,
78
+ ),
79
+ PlugwiseSensorEntityDescription(
80
+ key="temperature_difference",
81
+ translation_key="temperature_difference",
82
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
83
+ device_class=SensorDeviceClass.TEMPERATURE,
84
+ entity_category=EntityCategory.DIAGNOSTIC,
85
+ state_class=SensorStateClass.MEASUREMENT,
86
+ ),
87
+ PlugwiseSensorEntityDescription(
88
+ key="outdoor_temperature",
89
+ translation_key="outdoor_temperature",
90
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
91
+ device_class=SensorDeviceClass.TEMPERATURE,
92
+ entity_category=EntityCategory.DIAGNOSTIC,
93
+ state_class=SensorStateClass.MEASUREMENT,
94
+ ),
95
+ PlugwiseSensorEntityDescription(
96
+ key="outdoor_air_temperature",
97
+ translation_key="outdoor_air_temperature",
98
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
99
+ device_class=SensorDeviceClass.TEMPERATURE,
100
+ entity_category=EntityCategory.DIAGNOSTIC,
101
+ state_class=SensorStateClass.MEASUREMENT,
102
+ ),
103
+ PlugwiseSensorEntityDescription(
104
+ key="water_temperature",
105
+ translation_key="water_temperature",
106
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
107
+ device_class=SensorDeviceClass.TEMPERATURE,
108
+ entity_category=EntityCategory.DIAGNOSTIC,
109
+ state_class=SensorStateClass.MEASUREMENT,
110
+ ),
111
+ PlugwiseSensorEntityDescription(
112
+ key="return_temperature",
113
+ translation_key="return_temperature",
114
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
115
+ device_class=SensorDeviceClass.TEMPERATURE,
116
+ entity_category=EntityCategory.DIAGNOSTIC,
117
+ state_class=SensorStateClass.MEASUREMENT,
118
+ ),
119
+ PlugwiseSensorEntityDescription(
120
+ key="electricity_consumed",
121
+ translation_key="electricity_consumed",
122
+ native_unit_of_measurement=UnitOfPower.WATT,
123
+ device_class=SensorDeviceClass.POWER,
124
+ state_class=SensorStateClass.MEASUREMENT,
125
+ ),
126
+ PlugwiseSensorEntityDescription(
127
+ key="electricity_produced",
128
+ translation_key="electricity_produced",
129
+ native_unit_of_measurement=UnitOfPower.WATT,
130
+ device_class=SensorDeviceClass.POWER,
131
+ state_class=SensorStateClass.MEASUREMENT,
132
+ entity_registry_enabled_default=False,
133
+ ),
134
+ PlugwiseSensorEntityDescription(
135
+ key="electricity_consumed_interval",
136
+ translation_key="electricity_consumed_interval",
137
+ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
138
+ device_class=SensorDeviceClass.ENERGY,
139
+ state_class=SensorStateClass.TOTAL,
140
+ ),
141
+ PlugwiseSensorEntityDescription(
142
+ key="electricity_consumed_peak_interval",
143
+ translation_key="electricity_consumed_peak_interval",
144
+ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
145
+ device_class=SensorDeviceClass.ENERGY,
146
+ state_class=SensorStateClass.TOTAL,
147
+ ),
148
+ PlugwiseSensorEntityDescription(
149
+ key="electricity_consumed_off_peak_interval",
150
+ translation_key="electricity_consumed_off_peak_interval",
151
+ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
152
+ device_class=SensorDeviceClass.ENERGY,
153
+ state_class=SensorStateClass.TOTAL,
154
+ ),
155
+ PlugwiseSensorEntityDescription(
156
+ key="electricity_produced_interval",
157
+ translation_key="electricity_produced_interval",
158
+ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
159
+ device_class=SensorDeviceClass.ENERGY,
160
+ state_class=SensorStateClass.TOTAL,
161
+ entity_registry_enabled_default=False,
162
+ ),
163
+ PlugwiseSensorEntityDescription(
164
+ key="electricity_produced_peak_interval",
165
+ translation_key="electricity_produced_peak_interval",
166
+ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
167
+ device_class=SensorDeviceClass.ENERGY,
168
+ state_class=SensorStateClass.TOTAL,
169
+ ),
170
+ PlugwiseSensorEntityDescription(
171
+ key="electricity_produced_off_peak_interval",
172
+ translation_key="electricity_produced_off_peak_interval",
173
+ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
174
+ device_class=SensorDeviceClass.ENERGY,
175
+ state_class=SensorStateClass.TOTAL,
176
+ ),
177
+ PlugwiseSensorEntityDescription(
178
+ key="electricity_consumed_point",
179
+ translation_key="electricity_consumed_point",
180
+ device_class=SensorDeviceClass.POWER,
181
+ native_unit_of_measurement=UnitOfPower.WATT,
182
+ state_class=SensorStateClass.MEASUREMENT,
183
+ ),
184
+ PlugwiseSensorEntityDescription(
185
+ key="electricity_consumed_off_peak_point",
186
+ translation_key="electricity_consumed_off_peak_point",
187
+ native_unit_of_measurement=UnitOfPower.WATT,
188
+ device_class=SensorDeviceClass.POWER,
189
+ state_class=SensorStateClass.MEASUREMENT,
190
+ ),
191
+ PlugwiseSensorEntityDescription(
192
+ key="electricity_consumed_peak_point",
193
+ translation_key="electricity_consumed_peak_point",
194
+ native_unit_of_measurement=UnitOfPower.WATT,
195
+ device_class=SensorDeviceClass.POWER,
196
+ state_class=SensorStateClass.MEASUREMENT,
197
+ ),
198
+ PlugwiseSensorEntityDescription(
199
+ key="electricity_consumed_off_peak_cumulative",
200
+ translation_key="electricity_consumed_off_peak_cumulative",
201
+ native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
202
+ device_class=SensorDeviceClass.ENERGY,
203
+ state_class=SensorStateClass.TOTAL_INCREASING,
204
+ ),
205
+ PlugwiseSensorEntityDescription(
206
+ key="electricity_consumed_peak_cumulative",
207
+ translation_key="electricity_consumed_peak_cumulative",
208
+ native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
209
+ device_class=SensorDeviceClass.ENERGY,
210
+ state_class=SensorStateClass.TOTAL_INCREASING,
211
+ ),
212
+ PlugwiseSensorEntityDescription(
213
+ key="electricity_produced_point",
214
+ translation_key="electricity_produced_point",
215
+ device_class=SensorDeviceClass.POWER,
216
+ native_unit_of_measurement=UnitOfPower.WATT,
217
+ state_class=SensorStateClass.MEASUREMENT,
218
+ ),
219
+ PlugwiseSensorEntityDescription(
220
+ key="electricity_produced_off_peak_point",
221
+ translation_key="electricity_produced_off_peak_point",
222
+ native_unit_of_measurement=UnitOfPower.WATT,
223
+ device_class=SensorDeviceClass.POWER,
224
+ state_class=SensorStateClass.MEASUREMENT,
225
+ ),
226
+ PlugwiseSensorEntityDescription(
227
+ key="electricity_produced_peak_point",
228
+ translation_key="electricity_produced_peak_point",
229
+ native_unit_of_measurement=UnitOfPower.WATT,
230
+ device_class=SensorDeviceClass.POWER,
231
+ state_class=SensorStateClass.MEASUREMENT,
232
+ ),
233
+ PlugwiseSensorEntityDescription(
234
+ key="electricity_produced_off_peak_cumulative",
235
+ translation_key="electricity_produced_off_peak_cumulative",
236
+ native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
237
+ device_class=SensorDeviceClass.ENERGY,
238
+ state_class=SensorStateClass.TOTAL_INCREASING,
239
+ ),
240
+ PlugwiseSensorEntityDescription(
241
+ key="electricity_produced_peak_cumulative",
242
+ translation_key="electricity_produced_peak_cumulative",
243
+ native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
244
+ device_class=SensorDeviceClass.ENERGY,
245
+ state_class=SensorStateClass.TOTAL_INCREASING,
246
+ ),
247
+ PlugwiseSensorEntityDescription(
248
+ key="electricity_phase_one_consumed",
249
+ translation_key="electricity_phase_one_consumed",
250
+ device_class=SensorDeviceClass.POWER,
251
+ native_unit_of_measurement=UnitOfPower.WATT,
252
+ state_class=SensorStateClass.MEASUREMENT,
253
+ ),
254
+ PlugwiseSensorEntityDescription(
255
+ key="electricity_phase_two_consumed",
256
+ translation_key="electricity_phase_two_consumed",
257
+ device_class=SensorDeviceClass.POWER,
258
+ native_unit_of_measurement=UnitOfPower.WATT,
259
+ state_class=SensorStateClass.MEASUREMENT,
260
+ ),
261
+ PlugwiseSensorEntityDescription(
262
+ key="electricity_phase_three_consumed",
263
+ translation_key="electricity_phase_three_consumed",
264
+ device_class=SensorDeviceClass.POWER,
265
+ native_unit_of_measurement=UnitOfPower.WATT,
266
+ state_class=SensorStateClass.MEASUREMENT,
267
+ ),
268
+ PlugwiseSensorEntityDescription(
269
+ key="electricity_phase_one_produced",
270
+ translation_key="electricity_phase_one_produced",
271
+ device_class=SensorDeviceClass.POWER,
272
+ native_unit_of_measurement=UnitOfPower.WATT,
273
+ state_class=SensorStateClass.MEASUREMENT,
274
+ ),
275
+ PlugwiseSensorEntityDescription(
276
+ key="electricity_phase_two_produced",
277
+ translation_key="electricity_phase_two_produced",
278
+ device_class=SensorDeviceClass.POWER,
279
+ native_unit_of_measurement=UnitOfPower.WATT,
280
+ state_class=SensorStateClass.MEASUREMENT,
281
+ ),
282
+ PlugwiseSensorEntityDescription(
283
+ key="electricity_phase_three_produced",
284
+ translation_key="electricity_phase_three_produced",
285
+ device_class=SensorDeviceClass.POWER,
286
+ native_unit_of_measurement=UnitOfPower.WATT,
287
+ state_class=SensorStateClass.MEASUREMENT,
288
+ ),
289
+ PlugwiseSensorEntityDescription(
290
+ key="voltage_phase_one",
291
+ translation_key="voltage_phase_one",
292
+ device_class=SensorDeviceClass.VOLTAGE,
293
+ native_unit_of_measurement=UnitOfElectricPotential.VOLT,
294
+ state_class=SensorStateClass.MEASUREMENT,
295
+ entity_registry_enabled_default=False,
296
+ ),
297
+ PlugwiseSensorEntityDescription(
298
+ key="voltage_phase_two",
299
+ translation_key="voltage_phase_two",
300
+ device_class=SensorDeviceClass.VOLTAGE,
301
+ native_unit_of_measurement=UnitOfElectricPotential.VOLT,
302
+ state_class=SensorStateClass.MEASUREMENT,
303
+ entity_registry_enabled_default=False,
304
+ ),
305
+ PlugwiseSensorEntityDescription(
306
+ key="voltage_phase_three",
307
+ translation_key="voltage_phase_three",
308
+ device_class=SensorDeviceClass.VOLTAGE,
309
+ native_unit_of_measurement=UnitOfElectricPotential.VOLT,
310
+ state_class=SensorStateClass.MEASUREMENT,
311
+ entity_registry_enabled_default=False,
312
+ ),
313
+ PlugwiseSensorEntityDescription(
314
+ key="gas_consumed_interval",
315
+ translation_key="gas_consumed_interval",
316
+ native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
317
+ state_class=SensorStateClass.MEASUREMENT,
318
+ ),
319
+ PlugwiseSensorEntityDescription(
320
+ key="gas_consumed_cumulative",
321
+ translation_key="gas_consumed_cumulative",
322
+ native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
323
+ device_class=SensorDeviceClass.GAS,
324
+ state_class=SensorStateClass.TOTAL,
325
+ ),
326
+ PlugwiseSensorEntityDescription(
327
+ key="net_electricity_point",
328
+ translation_key="net_electricity_point",
329
+ native_unit_of_measurement=UnitOfPower.WATT,
330
+ device_class=SensorDeviceClass.POWER,
331
+ state_class=SensorStateClass.MEASUREMENT,
332
+ ),
333
+ PlugwiseSensorEntityDescription(
334
+ key="net_electricity_cumulative",
335
+ translation_key="net_electricity_cumulative",
336
+ native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
337
+ device_class=SensorDeviceClass.ENERGY,
338
+ state_class=SensorStateClass.TOTAL,
339
+ ),
340
+ PlugwiseSensorEntityDescription(
341
+ key="battery",
342
+ native_unit_of_measurement=PERCENTAGE,
343
+ device_class=SensorDeviceClass.BATTERY,
344
+ entity_category=EntityCategory.DIAGNOSTIC,
345
+ state_class=SensorStateClass.MEASUREMENT,
346
+ ),
347
+ PlugwiseSensorEntityDescription(
348
+ key="illuminance",
349
+ native_unit_of_measurement=LIGHT_LUX,
350
+ device_class=SensorDeviceClass.ILLUMINANCE,
351
+ entity_category=EntityCategory.DIAGNOSTIC,
352
+ state_class=SensorStateClass.MEASUREMENT,
353
+ ),
354
+ PlugwiseSensorEntityDescription(
355
+ key="modulation_level",
356
+ translation_key="modulation_level",
357
+ native_unit_of_measurement=PERCENTAGE,
358
+ entity_category=EntityCategory.DIAGNOSTIC,
359
+ state_class=SensorStateClass.MEASUREMENT,
360
+ ),
361
+ PlugwiseSensorEntityDescription(
362
+ key="valve_position",
363
+ translation_key="valve_position",
364
+ native_unit_of_measurement=PERCENTAGE,
365
+ entity_category=EntityCategory.DIAGNOSTIC,
366
+ state_class=SensorStateClass.MEASUREMENT,
367
+ ),
368
+ PlugwiseSensorEntityDescription(
369
+ key="water_pressure",
370
+ translation_key="water_pressure",
371
+ native_unit_of_measurement=UnitOfPressure.BAR,
372
+ device_class=SensorDeviceClass.PRESSURE,
373
+ entity_category=EntityCategory.DIAGNOSTIC,
374
+ state_class=SensorStateClass.MEASUREMENT,
375
+ ),
376
+ PlugwiseSensorEntityDescription(
377
+ key="humidity",
378
+ native_unit_of_measurement=PERCENTAGE,
379
+ device_class=SensorDeviceClass.HUMIDITY,
380
+ state_class=SensorStateClass.MEASUREMENT,
381
+ ),
382
+ PlugwiseSensorEntityDescription(
383
+ key="dhw_temperature",
384
+ translation_key="dhw_temperature",
385
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
386
+ device_class=SensorDeviceClass.TEMPERATURE,
387
+ entity_category=EntityCategory.DIAGNOSTIC,
388
+ state_class=SensorStateClass.MEASUREMENT,
389
+ ),
390
+ PlugwiseSensorEntityDescription(
391
+ key="domestic_hot_water_setpoint",
392
+ translation_key="domestic_hot_water_setpoint",
393
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
394
+ device_class=SensorDeviceClass.TEMPERATURE,
395
+ entity_category=EntityCategory.DIAGNOSTIC,
396
+ state_class=SensorStateClass.MEASUREMENT,
397
+ ),
398
+ )
399
+
400
+
401
  async def async_setup_entry(
402
  hass: HomeAssistant,
403
+ entry: PlugwiseConfigEntry,
404
+ async_add_entities: AddConfigEntryEntitiesCallback,
405
  ) -> None:
406
  """Set up the Smile sensors from a config entry."""
407
+ coordinator = entry.runtime_data
408
 
409
+ @callback
410
+ def _add_entities() -> None:
411
+ """Add Entities."""
412
+ if not coordinator.new_devices:
413
+ return
414
+
415
+ async_add_entities(
416
+ PlugwiseSensorEntity(coordinator, device_id, description)
417
+ for device_id in coordinator.new_devices
418
+ if (sensors := coordinator.data[device_id].get("sensors"))
419
+ for description in SENSORS
420
+ if description.key in sensors
421
+ )
 
 
 
 
422
 
423
+ _add_entities()
424
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
425
 
426
 
427
  class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
428
  """Represent Plugwise Sensors."""
429
 
430
+ entity_description: PlugwiseSensorEntityDescription
431
+
432
  def __init__(
433
  self,
434
  coordinator: PlugwiseDataUpdateCoordinator,
 
437
  ) -> None:
438
  """Initialise the sensor."""
439
  super().__init__(coordinator, device_id)
 
440
  self._attr_unique_id = f"{device_id}-{description.key}"
441
+ self.entity_description = description
442
 
443
  @property
444
+ @override
445
+ def native_value(self) -> int | float:
446
  """Return the value reported by the sensor."""
447
+ return self.device["sensors"][self.entity_description.key]
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/strings.json RENAMED
@@ -1,103 +1,335 @@
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)",
13
- "homekit_emulation": "Homekit emulation (i.e. on hvac_off => Away)",
14
- "refresh_interval": "Frontend refresh-time (1.5 - 5 seconds)"
15
- }
16
- }
17
- }
18
- },
19
  "config": {
20
- "step": {
21
- "user": {
22
- "title": "Plugwise connection type",
23
- "description": "Please select:",
24
- "data": {
25
- "flow_type": "Connection type"
26
- }
27
- },
28
- "user_gateway": {
29
- "title": "Connect to the Plugwise Adam/Smile/Stretch",
30
- "description": "Please enter:",
31
- "data": {
32
- "password": "ID",
33
- "username": "Username",
34
- "host": "IP-address",
35
- "port": "Port number"
36
- }
37
- },
38
- "user_usb": {
39
- "title": "Connect to Plugwise Stick",
40
- "description": "Please enter:",
41
- "data": {
42
- "usb_path": "[%key:common::config_flow::data::usb_path%]"
43
- }
44
- },
45
- "manual_path": {
46
- "data": {
47
- "usb_path": "[%key:common::config_flow::data::usb_path%]"
48
- }
49
- }
50
  },
51
  "error": {
52
  "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
53
- "invalid_auth": "Authentication failed",
54
  "invalid_setup": "Add your Adam instead of your Anna, see the documentation",
55
- "network_down": "Plugwise Zigbee network is down",
56
- "network_timeout": "Network communication timeout",
57
  "response_error": "Invalid XML data, or error indication received",
58
- "stick_init": "Initialization of Plugwise USB-stick failed",
59
  "unknown": "[%key:common::config_flow::error::unknown%]",
60
  "unsupported": "Device with unsupported firmware"
61
  },
62
- "abort": {
63
- "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
64
- "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
66
  },
67
  "entity": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  "climate": {
69
  "plugwise": {
70
  "state_attributes": {
 
 
 
71
  "preset_mode": {
72
  "state": {
73
  "asleep": "Night",
74
- "away": "Away",
75
- "home": "Home",
76
  "no_frost": "Anti-frost",
77
  "vacation": "Vacation"
78
  }
 
 
 
79
  }
80
  }
81
  }
82
  },
 
 
 
 
 
 
 
 
 
 
 
83
  "select": {
84
- "dhw_mode": {
 
85
  "state": {
86
- "off": "Off",
87
- "auto": "Auto",
88
- "boost": "Boost",
89
- "comfort": "Comfort"
90
  }
91
  },
92
- "regulation_mode": {
 
 
 
 
 
 
 
 
 
93
  "state": {
94
  "bleeding_cold": "Bleeding cold",
95
  "bleeding_hot": "Bleeding hot",
96
- "cooling": "Cooling",
97
- "heating": "Heating",
98
- "off": "Off"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  }
102
  }
103
  }
 
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": {
48
+ "binary_sensor": {
49
+ "compressor_state": {
50
+ "name": "Compressor state"
51
+ },
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
+ },
61
+ "flame_state": {
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_setup": {
323
+ "message": "Add your Adam instead of your Anna, see the documentation"
324
+ },
325
+ "response_error": {
326
+ "message": "[%key:component::plugwise::config::error::response_error%]"
327
+ },
328
+ "set_schedule_first": {
329
+ "message": "Failed setting HVACMode, set a schedule first."
330
+ },
331
+ "unsupported_firmware": {
332
+ "message": "[%key:component::plugwise::config::error::unsupported%]"
333
  }
334
  }
335
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py RENAMED
@@ -1,44 +1,88 @@
1
  """Plugwise Switch component for HomeAssistant."""
2
- from __future__ import annotations
3
 
4
- from typing import Any
 
5
 
6
- from homeassistant.components.switch import SwitchEntity
7
- from homeassistant.config_entries import ConfigEntry
8
- from homeassistant.core import HomeAssistant
9
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
10
 
11
- from .const import COORDINATOR, DOMAIN, LOGGER, SMILE
12
- from .coordinator import PlugwiseDataUpdateCoordinator
 
 
 
 
 
 
 
 
13
  from .entity import PlugwiseEntity
14
- from .models import PW_SWITCH_TYPES, PlugwiseSwitchEntityDescription
15
  from .util import plugwise_command
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  async def async_setup_entry(
19
  hass: HomeAssistant,
20
- config_entry: ConfigEntry,
21
- async_add_entities: AddEntitiesCallback,
22
  ) -> None:
23
  """Set up the Smile switches from a config entry."""
24
- coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
25
- entities: list[PlugwiseSwitchEntity] = []
26
- for device_id, device in coordinator.data.devices.items():
27
- for description in PW_SWITCH_TYPES:
28
- if (
29
- "switches" not in device
30
- or description.key not in device["switches"]
31
- or description.plugwise_api != SMILE
32
- ):
33
- continue
34
- entities.append(PlugwiseSwitchEntity(coordinator, device_id, description))
35
- LOGGER.debug("Add %s switch", description.key)
36
- async_add_entities(entities)
 
 
 
 
 
37
 
38
 
39
  class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
40
  """Representation of a Plugwise plug."""
41
 
 
 
42
  def __init__(
43
  self,
44
  coordinator: PlugwiseDataUpdateCoordinator,
@@ -47,15 +91,17 @@
47
  ) -> None:
48
  """Set up the Plugwise API."""
49
  super().__init__(coordinator, device_id)
50
- self.entity_description = description
51
  self._attr_unique_id = f"{device_id}-{description.key}"
 
52
 
53
  @property
54
- def is_on(self) -> bool | None:
 
55
  """Return True if entity is on."""
56
- return self.device["switches"].get(self.entity_description.key)
57
 
58
  @plugwise_command
 
59
  async def async_turn_on(self, **kwargs: Any) -> None:
60
  """Turn the device on."""
61
  await self.coordinator.api.set_switch_state(
@@ -66,6 +112,7 @@
66
  )
67
 
68
  @plugwise_command
 
69
  async def async_turn_off(self, **kwargs: Any) -> None:
70
  """Turn the device off."""
71
  await self.coordinator.api.set_switch_state(
 
1
  """Plugwise Switch component for HomeAssistant."""
 
2
 
3
+ from dataclasses import dataclass
4
+ from typing import Any, override
5
 
6
+ from plugwise.constants import SwitchType
 
 
 
7
 
8
+ from homeassistant.components.switch import (
9
+ SwitchDeviceClass,
10
+ SwitchEntity,
11
+ SwitchEntityDescription,
12
+ )
13
+ from homeassistant.const import EntityCategory
14
+ from homeassistant.core import HomeAssistant, callback
15
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
16
+
17
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
18
  from .entity import PlugwiseEntity
 
19
  from .util import plugwise_command
20
 
21
+ PARALLEL_UPDATES = 0
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class PlugwiseSwitchEntityDescription(SwitchEntityDescription):
26
+ """Describes Plugwise switch entity."""
27
+
28
+ key: SwitchType
29
+
30
+
31
+ SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
32
+ PlugwiseSwitchEntityDescription(
33
+ key="dhw_cm_switch",
34
+ translation_key="dhw_cm_switch",
35
+ entity_category=EntityCategory.CONFIG,
36
+ ),
37
+ PlugwiseSwitchEntityDescription(
38
+ key="lock",
39
+ translation_key="lock",
40
+ entity_category=EntityCategory.CONFIG,
41
+ ),
42
+ PlugwiseSwitchEntityDescription(
43
+ key="relay",
44
+ translation_key="relay",
45
+ device_class=SwitchDeviceClass.SWITCH,
46
+ ),
47
+ PlugwiseSwitchEntityDescription(
48
+ key="cooling_ena_switch",
49
+ translation_key="cooling_ena_switch",
50
+ entity_category=EntityCategory.CONFIG,
51
+ ),
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 switches 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
+ async_add_entities(
70
+ PlugwiseSwitchEntity(coordinator, device_id, description)
71
+ for device_id in coordinator.new_devices
72
+ if (switches := coordinator.data[device_id].get("switches"))
73
+ for description in SWITCHES
74
+ if description.key in switches
75
+ )
76
+
77
+ _add_entities()
78
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
79
 
80
 
81
  class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
82
  """Representation of a Plugwise plug."""
83
 
84
+ entity_description: PlugwiseSwitchEntityDescription
85
+
86
  def __init__(
87
  self,
88
  coordinator: PlugwiseDataUpdateCoordinator,
 
91
  ) -> None:
92
  """Set up the Plugwise API."""
93
  super().__init__(coordinator, device_id)
 
94
  self._attr_unique_id = f"{device_id}-{description.key}"
95
+ self.entity_description = description
96
 
97
  @property
98
+ @override
99
+ def is_on(self) -> bool:
100
  """Return True if entity is on."""
101
+ return self.device["switches"][self.entity_description.key]
102
 
103
  @plugwise_command
104
+ @override
105
  async def async_turn_on(self, **kwargs: Any) -> None:
106
  """Turn the device on."""
107
  await self.coordinator.api.set_switch_state(
 
112
  )
113
 
114
  @plugwise_command
115
+ @override
116
  async def async_turn_off(self, **kwargs: Any) -> None:
117
  """Turn the device off."""
118
  await self.coordinator.api.set_switch_state(
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py RENAMED
@@ -1,21 +1,18 @@
1
  """Utilities for Plugwise."""
 
2
  from collections.abc import Awaitable, Callable, Coroutine
3
- from typing import Any, Concatenate, TypeVar
4
 
5
- from typing_extensions import ParamSpec
6
 
7
  from homeassistant.exceptions import HomeAssistantError
8
- from plugwise.exceptions import PlugwiseException
9
 
 
10
  from .entity import PlugwiseEntity
11
 
12
- _PlugwiseEntityT = TypeVar("_PlugwiseEntityT", bound=PlugwiseEntity)
13
- _R = TypeVar("_R")
14
- _P = ParamSpec("_P")
15
-
16
 
17
- def plugwise_command(
18
- func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]]
19
  ) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
20
  """Decorate Plugwise calls that send commands/make changes to the device.
21
 
@@ -28,10 +25,14 @@
28
  ) -> _R:
29
  try:
30
  return await func(self, *args, **kwargs)
31
- except PlugwiseException as error:
32
  raise HomeAssistantError(
33
- f"Error communicating with API: {error}"
34
- ) from error
 
 
 
 
35
  finally:
36
  await self.coordinator.async_request_refresh()
37
 
 
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