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