Diff to HTML by rtfpessoa

Files changed (17) hide show
  1. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/__init__.py +26 -97
  2. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/binary_sensor.py +44 -102
  3. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/button.py +13 -25
  4. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py +188 -166
  5. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py +111 -159
  6. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py +35 -167
  7. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py +142 -82
  8. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/diagnostics.py +2 -7
  9. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py +31 -45
  10. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/icons.json +6 -9
  11. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json +4 -4
  12. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py +29 -60
  13. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py +40 -58
  14. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py +109 -191
  15. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/strings.json +189 -159
  16. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py +31 -61
  17. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py +8 -10
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/__init__.py RENAMED
@@ -1,50 +1,22 @@
1
  """Plugwise platform for Home Assistant Core."""
2
 
3
- from __future__ import annotations
4
-
5
  from typing import Any
6
 
7
- from plugwise.exceptions import PlugwiseError
8
- import voluptuous as vol # pw-beta delete_notification
9
-
10
- from homeassistant.config_entries import ConfigEntry
11
- from homeassistant.const import CONF_TIMEOUT, Platform
12
- from homeassistant.core import (
13
- HomeAssistant,
14
- ServiceCall, # pw-beta delete_notification
15
- callback,
16
- )
17
  from homeassistant.helpers import device_registry as dr, entity_registry as er
18
 
19
- from .const import (
20
- CONF_REFRESH_INTERVAL, # pw-beta options
21
- DOMAIN,
22
- LOGGER,
23
- PLATFORMS,
24
- SERVICE_DELETE, # pw-beta delete_notifications
25
- )
26
- from .coordinator import PlugwiseDataUpdateCoordinator
27
-
28
- type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
29
 
30
 
31
  async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
32
- """Set up Plugwise from a config entry."""
33
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
34
 
35
- cooldown = 1.5 # pw-beta frontend refresh-interval
36
- if (
37
- custom_refresh := entry.options.get(CONF_REFRESH_INTERVAL)
38
- ) is not None: # pragma: no cover
39
- cooldown = custom_refresh
40
- LOGGER.debug("DUC cooldown interval: %s", cooldown)
41
-
42
- coordinator = PlugwiseDataUpdateCoordinator(
43
- hass, cooldown
44
- ) # pw-beta - cooldown, update_interval as extra
45
  await coordinator.async_config_entry_first_refresh()
46
-
47
- await async_migrate_sensor_entities(hass, coordinator)
48
 
49
  entry.runtime_data = coordinator
50
 
@@ -53,55 +25,28 @@
53
  config_entry_id=entry.entry_id,
54
  identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
55
  manufacturer="Plugwise",
56
- model=coordinator.api.smile_model,
57
- model_id=coordinator.api.smile_model_id,
58
- name=coordinator.api.smile_name,
59
- sw_version=str(coordinator.api.smile_version),
60
  ) # required for adding the entity-less P1 Gateway
61
 
62
- async def delete_notification(
63
- call: ServiceCall,
64
- ) -> None: # pragma: no cover # pw-beta: HA service - delete_notification
65
- """Service: delete the Plugwise Notification."""
66
- LOGGER.debug(
67
- "Service delete PW Notification called for %s",
68
- coordinator.api.smile_name,
69
- )
70
- try:
71
- await coordinator.api.delete_notification()
72
- LOGGER.debug("PW Notification deleted")
73
- except PlugwiseError:
74
- LOGGER.debug(
75
- "Failed to delete the Plugwise Notification for %s",
76
- coordinator.api.smile_name,
77
- )
78
-
79
  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
80
 
81
- entry.async_on_unload(entry.add_update_listener(update_listener)) # pw-beta options_flow
82
- for component in PLATFORMS: # pw-beta delete_notification
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
- async def update_listener(
91
- hass: HomeAssistant, entry: PlugwiseConfigEntry
92
- ) -> None: # pragma: no cover # pw-beta
93
- """Handle options update."""
94
- await hass.config_entries.async_reload(entry.entry_id)
95
 
96
  async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
97
- """Unload Plugwise."""
98
  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
99
 
 
100
  @callback
101
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
102
  """Migrate Plugwise entity entries.
103
 
104
- - Migrates old unique ID's from old binary_sensors and switches to the new unique ID's
 
105
  """
106
  if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
107
  "-slave_boiler_state"
@@ -123,17 +68,18 @@
123
  # No migration needed
124
  return None
125
 
126
- async def async_migrate_sensor_entities(
 
127
  hass: HomeAssistant,
128
  coordinator: PlugwiseDataUpdateCoordinator,
129
  ) -> None:
130
  """Migrate Sensors if needed."""
131
  ent_reg = er.async_get(hass)
132
 
133
- # Migrate opentherm_outdoor_temperature
134
  # to opentherm_outdoor_air_temperature sensor
135
- for device_id, device in coordinator.data.devices.items():
136
- if device["dev_class"] != "heater_central":
137
  continue
138
 
139
  old_unique_id = f"{device_id}-outdoor_temperature"
@@ -141,27 +87,10 @@
141
  Platform.SENSOR, DOMAIN, old_unique_id
142
  ):
143
  new_unique_id = f"{device_id}-outdoor_air_temperature"
144
- # Upstream remove LOGGER debug
 
 
 
 
 
145
  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
146
-
147
- # pw-beta only - revert adding CONF_TIMEOUT to config_entry in v0.53.3
148
- async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
149
- """Migrate back to v1.1 config entry."""
150
- if entry.version > 1:
151
- # This means the user has downgraded from a future version
152
- return False
153
-
154
- if entry.version == 1 and entry.minor_version == 2:
155
- new_data = {**entry.data}
156
- new_data.pop(CONF_TIMEOUT)
157
- hass.config_entries.async_update_entry(
158
- entry, data=new_data, minor_version=1, version=1
159
- )
160
-
161
- LOGGER.debug(
162
- "Migration to version %s.%s successful",
163
- entry.version,
164
- entry.minor_version,
165
- )
166
-
167
- return True
 
1
  """Plugwise platform for Home Assistant Core."""
2
 
 
 
3
  from typing import Any
4
 
5
+ from homeassistant.const import Platform
6
+ from homeassistant.core import HomeAssistant, callback
 
 
 
 
 
 
 
 
7
  from homeassistant.helpers import device_registry as dr, entity_registry as er
8
 
9
+ from .const import DEV_CLASS, DOMAIN, LOGGER, PLATFORMS
10
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
 
 
 
11
 
12
 
13
  async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
14
+ """Set up Plugwise components from a config entry."""
15
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
16
 
17
+ coordinator = PlugwiseDataUpdateCoordinator(hass, entry)
 
 
 
 
 
 
 
 
 
18
  await coordinator.async_config_entry_first_refresh()
19
+ migrate_sensor_entities(hass, coordinator)
 
20
 
21
  entry.runtime_data = coordinator
22
 
 
25
  config_entry_id=entry.entry_id,
26
  identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
27
  manufacturer="Plugwise",
28
+ model=coordinator.api.smile.model,
29
+ model_id=coordinator.api.smile.model_id,
30
+ name=coordinator.api.smile.name,
31
+ sw_version=str(coordinator.api.smile.version),
32
  ) # required for adding the entity-less P1 Gateway
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
35
 
 
 
 
 
 
 
 
36
  return True
37
 
 
 
 
 
 
38
 
39
  async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
40
+ """Unload the Plugwise components."""
41
  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
42
 
43
+
44
  @callback
45
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
46
  """Migrate Plugwise entity entries.
47
 
48
+ Migrates old unique ID's from old binary_sensors and
49
+ switches to the new unique ID's.
50
  """
51
  if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
52
  "-slave_boiler_state"
 
68
  # No migration needed
69
  return None
70
 
71
+
72
+ def migrate_sensor_entities(
73
  hass: HomeAssistant,
74
  coordinator: PlugwiseDataUpdateCoordinator,
75
  ) -> None:
76
  """Migrate Sensors if needed."""
77
  ent_reg = er.async_get(hass)
78
 
79
+ # Migrating opentherm_outdoor_temperature
80
  # to opentherm_outdoor_air_temperature sensor
81
+ for device_id, device in coordinator.data.items():
82
+ if device[DEV_CLASS] != "heater_central":
83
  continue
84
 
85
  old_unique_id = f"{device_id}-outdoor_temperature"
 
87
  Platform.SENSOR, DOMAIN, old_unique_id
88
  ):
89
  new_unique_id = f"{device_id}-outdoor_air_temperature"
90
+ LOGGER.debug(
91
+ "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
92
+ entity_id,
93
+ old_unique_id,
94
+ new_unique_id,
95
+ )
96
  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/binary_sensor.py RENAMED
@@ -1,10 +1,8 @@
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
 
@@ -13,34 +11,17 @@
13
  BinarySensorEntity,
14
  BinarySensorEntityDescription,
15
  )
16
- import homeassistant.components.persistent_notification as pn # pw-beta Plugwise notifications
17
  from homeassistant.const import EntityCategory
18
  from homeassistant.core import HomeAssistant, callback
19
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
20
-
21
- from . import PlugwiseConfigEntry
22
- from .const import (
23
- BATTERY_STATE,
24
- BINARY_SENSORS,
25
- COMPRESSOR_STATE,
26
- COOLING_ENABLED,
27
- COOLING_STATE,
28
- DHW_STATE,
29
- DOMAIN,
30
- FLAME_STATE,
31
- HEATING_STATE,
32
- LOGGER, # pw-beta
33
- NOTIFICATIONS,
34
- PLUGWISE_NOTIFICATION,
35
- SECONDARY_BOILER_STATE,
36
- SEVERITIES,
37
- )
38
 
39
- # Upstream
40
- from .coordinator import PlugwiseDataUpdateCoordinator
41
  from .entity import PlugwiseEntity
42
 
43
- PARALLEL_UPDATES = 0 # Upstream
 
 
 
44
 
45
 
46
  @dataclass(frozen=True)
@@ -50,52 +31,50 @@
50
  key: BinarySensorType
51
 
52
 
53
- # Upstream PLUGWISE_BINARY_SENSORS
54
- PLUGWISE_BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
55
  PlugwiseBinarySensorEntityDescription(
56
- key=BATTERY_STATE,
57
- translation_key=BATTERY_STATE,
58
  device_class=BinarySensorDeviceClass.BATTERY,
59
  entity_category=EntityCategory.DIAGNOSTIC,
60
  ),
61
  PlugwiseBinarySensorEntityDescription(
62
- key=COMPRESSOR_STATE,
63
- translation_key=COMPRESSOR_STATE,
64
  entity_category=EntityCategory.DIAGNOSTIC,
65
  ),
66
  PlugwiseBinarySensorEntityDescription(
67
- key=COOLING_ENABLED,
68
- translation_key=COOLING_ENABLED,
69
  entity_category=EntityCategory.DIAGNOSTIC,
70
  ),
71
  PlugwiseBinarySensorEntityDescription(
72
- key=DHW_STATE,
73
- translation_key=DHW_STATE,
74
  entity_category=EntityCategory.DIAGNOSTIC,
75
  ),
76
  PlugwiseBinarySensorEntityDescription(
77
- key=FLAME_STATE,
78
- translation_key=FLAME_STATE,
79
  entity_category=EntityCategory.DIAGNOSTIC,
80
  ),
81
  PlugwiseBinarySensorEntityDescription(
82
- key=HEATING_STATE,
83
- translation_key=HEATING_STATE,
84
  entity_category=EntityCategory.DIAGNOSTIC,
85
  ),
86
  PlugwiseBinarySensorEntityDescription(
87
- key=COOLING_STATE,
88
- translation_key=COOLING_STATE,
89
  entity_category=EntityCategory.DIAGNOSTIC,
90
  ),
91
  PlugwiseBinarySensorEntityDescription(
92
- key=SECONDARY_BOILER_STATE,
93
- translation_key=SECONDARY_BOILER_STATE,
94
  entity_category=EntityCategory.DIAGNOSTIC,
95
  ),
96
  PlugwiseBinarySensorEntityDescription(
97
- key=PLUGWISE_NOTIFICATION,
98
- translation_key=PLUGWISE_NOTIFICATION,
99
  entity_category=EntityCategory.DIAGNOSTIC,
100
  ),
101
  )
@@ -104,9 +83,9 @@
104
  async def async_setup_entry(
105
  hass: HomeAssistant,
106
  entry: PlugwiseConfigEntry,
107
- async_add_entities: AddEntitiesCallback,
108
  ) -> None:
109
- """Set up Plugwise binary_sensors from a config entry."""
110
  coordinator = entry.runtime_data
111
 
112
  @callback
@@ -115,40 +94,20 @@
115
  if not coordinator.new_devices:
116
  return
117
 
118
- # Upstream consts to HA
119
- # async_add_entities(
120
- # PlugwiseBinarySensorEntity(coordinator, device_id, description)
121
- # for device_id in coordinator.new_devices
122
- # if (
123
- # binary_sensors := coordinator.data.devices[device_id].get(
124
- # BINARY_SENSORS
125
- # )
126
- # )
127
- # for description in PLUGWISE_BINARY_SENSORS
128
- # if description.key in binary_sensors
129
- # )
130
-
131
- # pw-beta alternative for debugging
132
- entities: list[PlugwiseBinarySensorEntity] = []
133
- for device_id in coordinator.new_devices:
134
- device = coordinator.data.devices[device_id]
135
- if not (binary_sensors := device.get(BINARY_SENSORS)):
136
- continue
137
- for description in PLUGWISE_BINARY_SENSORS:
138
- if description.key not in binary_sensors:
139
- continue
140
- entities.append(PlugwiseBinarySensorEntity(coordinator, device_id, description))
141
- LOGGER.debug(
142
- "Add %s %s binary sensor", device["name"], description.translation_key
143
- )
144
- async_add_entities(entities)
145
 
146
  _add_entities()
147
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
148
 
149
 
150
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
151
- """Set up Plugwise binary_sensors from a config entry."""
152
 
153
  entity_description: PlugwiseBinarySensorEntityDescription
154
 
@@ -162,45 +121,28 @@
162
  super().__init__(coordinator, device_id)
163
  self.entity_description = description
164
  self._attr_unique_id = f"{device_id}-{description.key}"
165
- self._notification: dict[str, str] = {} # pw-beta
166
 
167
  @property
 
168
  def is_on(self) -> bool:
169
  """Return true if the binary sensor is on."""
170
- # pw-beta: show Plugwise notifications as HA persistent notifications
171
- if self._notification:
172
- for notify_id, message in self._notification.items():
173
- pn.async_create(
174
- self.hass, message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
175
- )
176
-
177
- return self.device[BINARY_SENSORS][self.entity_description.key]
178
 
179
  @property
 
180
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
181
  """Return entity specific state attributes."""
182
- if self.entity_description.key != PLUGWISE_NOTIFICATION: # Upstream const
183
  return None
184
 
185
- # pw-beta adjustment with attrs is to only represent severities *with* content
186
- # not all severities including those without content as empty lists
187
- attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
188
- self._notification = {} # pw-beta
189
- if notify := self.coordinator.data.gateway[NOTIFICATIONS]:
190
- for notify_id, details in notify.items(): # pw-beta uses notify_id
191
  for msg_type, msg in details.items():
192
  msg_type = msg_type.lower()
193
  if msg_type not in SEVERITIES:
194
- msg_type = "other" # pragma: no cover
195
-
196
- if (
197
- f"{msg_type}_msg" not in attrs
198
- ): # pw-beta Re-evaluate against Core
199
- attrs[f"{msg_type}_msg"] = []
200
  attrs[f"{msg_type}_msg"].append(msg)
201
 
202
- self._notification[
203
- notify_id
204
- ] = f"{msg_type.title()}: {msg}" # pw-beta
205
-
206
  return attrs
 
1
  """Plugwise Binary Sensor component for Home Assistant."""
2
 
 
 
3
  from collections.abc import Mapping
4
  from dataclasses import dataclass
5
+ from typing import Any, override
6
 
7
  from plugwise.constants import BinarySensorType
8
 
 
11
  BinarySensorEntity,
12
  BinarySensorEntityDescription,
13
  )
 
14
  from homeassistant.const import EntityCategory
15
  from homeassistant.core import HomeAssistant, callback
16
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
19
  from .entity import PlugwiseEntity
20
 
21
+ SEVERITIES = ["other", "info", "warning", "error"]
22
+
23
+ # Coordinator is used to centralize the data updates
24
+ PARALLEL_UPDATES = 0
25
 
26
 
27
  @dataclass(frozen=True)
 
31
  key: BinarySensorType
32
 
33
 
34
+ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
 
35
  PlugwiseBinarySensorEntityDescription(
36
+ key="low_battery",
 
37
  device_class=BinarySensorDeviceClass.BATTERY,
38
  entity_category=EntityCategory.DIAGNOSTIC,
39
  ),
40
  PlugwiseBinarySensorEntityDescription(
41
+ key="compressor_state",
42
+ translation_key="compressor_state",
43
  entity_category=EntityCategory.DIAGNOSTIC,
44
  ),
45
  PlugwiseBinarySensorEntityDescription(
46
+ key="cooling_enabled",
47
+ translation_key="cooling_enabled",
48
  entity_category=EntityCategory.DIAGNOSTIC,
49
  ),
50
  PlugwiseBinarySensorEntityDescription(
51
+ key="dhw_state",
52
+ translation_key="dhw_state",
53
  entity_category=EntityCategory.DIAGNOSTIC,
54
  ),
55
  PlugwiseBinarySensorEntityDescription(
56
+ key="flame_state",
57
+ translation_key="flame_state",
58
  entity_category=EntityCategory.DIAGNOSTIC,
59
  ),
60
  PlugwiseBinarySensorEntityDescription(
61
+ key="heating_state",
62
+ translation_key="heating_state",
63
  entity_category=EntityCategory.DIAGNOSTIC,
64
  ),
65
  PlugwiseBinarySensorEntityDescription(
66
+ key="cooling_state",
67
+ translation_key="cooling_state",
68
  entity_category=EntityCategory.DIAGNOSTIC,
69
  ),
70
  PlugwiseBinarySensorEntityDescription(
71
+ key="secondary_boiler_state",
72
+ translation_key="secondary_boiler_state",
73
  entity_category=EntityCategory.DIAGNOSTIC,
74
  ),
75
  PlugwiseBinarySensorEntityDescription(
76
+ key="plugwise_notification",
77
+ translation_key="plugwise_notification",
78
  entity_category=EntityCategory.DIAGNOSTIC,
79
  ),
80
  )
 
83
  async def async_setup_entry(
84
  hass: HomeAssistant,
85
  entry: PlugwiseConfigEntry,
86
+ async_add_entities: AddConfigEntryEntitiesCallback,
87
  ) -> None:
88
+ """Set up the Smile binary_sensors from a config entry."""
89
  coordinator = entry.runtime_data
90
 
91
  @callback
 
94
  if not coordinator.new_devices:
95
  return
96
 
97
+ async_add_entities(
98
+ PlugwiseBinarySensorEntity(coordinator, device_id, description)
99
+ for device_id in coordinator.new_devices
100
+ if (binary_sensors := coordinator.data[device_id].get("binary_sensors"))
101
+ for description in BINARY_SENSORS
102
+ if description.key in binary_sensors
103
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  _add_entities()
106
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
107
 
108
 
109
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
110
+ """Represent Smile Binary Sensors."""
111
 
112
  entity_description: PlugwiseBinarySensorEntityDescription
113
 
 
121
  super().__init__(coordinator, device_id)
122
  self.entity_description = description
123
  self._attr_unique_id = f"{device_id}-{description.key}"
 
124
 
125
  @property
126
+ @override
127
  def is_on(self) -> bool:
128
  """Return true if the binary sensor is on."""
129
+ return self.device["binary_sensors"][self.entity_description.key]
 
 
 
 
 
 
 
130
 
131
  @property
132
+ @override
133
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
134
  """Return entity specific state attributes."""
135
+ if self.entity_description.key != "plugwise_notification":
136
  return None
137
 
138
+ attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES}
139
+ gateway_id = self.coordinator.api.gateway_id
140
+ if notify := self.coordinator.data[gateway_id]["notifications"]:
141
+ for details in notify.values():
 
 
142
  for msg_type, msg in details.items():
143
  msg_type = msg_type.lower()
144
  if msg_type not in SEVERITIES:
145
+ msg_type = "other"
 
 
 
 
 
146
  attrs[f"{msg_type}_msg"].append(msg)
147
 
 
 
 
 
148
  return attrs
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/button.py RENAMED
@@ -1,46 +1,33 @@
1
  """Plugwise Button component for Home Assistant."""
2
 
3
- from __future__ import annotations
4
 
5
  from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
6
  from homeassistant.const import EntityCategory
7
  from homeassistant.core import HomeAssistant
8
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
9
 
10
- from . import PlugwiseConfigEntry
11
- from .const import (
12
- GATEWAY_ID,
13
- LOGGER, # pw-betea
14
- REBOOT,
15
- )
16
- from .coordinator import PlugwiseDataUpdateCoordinator
17
  from .entity import PlugwiseEntity
18
  from .util import plugwise_command
19
 
20
- PARALLEL_UPDATES = 0 # Upstream
21
 
22
 
23
  async def async_setup_entry(
24
  hass: HomeAssistant,
25
  entry: PlugwiseConfigEntry,
26
- async_add_entities: AddEntitiesCallback,
27
  ) -> None:
28
- """Set up Plugwise buttons from a config entry."""
29
  coordinator = entry.runtime_data
30
 
31
- gateway = coordinator.data.gateway
32
- # async_add_entities(
33
- # PlugwiseButtonEntity(coordinator, device_id)
34
- # for device_id in coordinator.data.devices
35
- # if device_id == gateway[GATEWAY_ID] and REBOOT in gateway
36
- # )
37
- # pw-beta alternative for debugging
38
- entities: list[PlugwiseButtonEntity] = []
39
- for device_id, device in coordinator.data.devices.items():
40
- if device_id == gateway[GATEWAY_ID] and REBOOT in gateway:
41
- entities.append(PlugwiseButtonEntity(coordinator, device_id))
42
- LOGGER.debug("Add %s reboot button", device["name"])
43
- async_add_entities(entities)
44
 
45
 
46
  class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
@@ -60,6 +47,7 @@
60
  self._attr_unique_id = f"{device_id}-reboot"
61
 
62
  @plugwise_command
 
63
  async def async_press(self) -> None:
64
  """Triggers the Plugwise button press service."""
65
  await self.coordinator.api.reboot_gateway()
 
1
  """Plugwise Button component for Home Assistant."""
2
 
3
+ from typing import override
4
 
5
  from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
6
  from homeassistant.const import EntityCategory
7
  from homeassistant.core import HomeAssistant
8
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
9
 
10
+ from .const import REBOOT
11
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
12
  from .entity import PlugwiseEntity
13
  from .util import plugwise_command
14
 
15
+ PARALLEL_UPDATES = 0
16
 
17
 
18
  async def async_setup_entry(
19
  hass: HomeAssistant,
20
  entry: PlugwiseConfigEntry,
21
+ async_add_entities: AddConfigEntryEntitiesCallback,
22
  ) -> None:
23
+ """Set up the Plugwise buttons from a ConfigEntry."""
24
  coordinator = entry.runtime_data
25
 
26
+ async_add_entities(
27
+ PlugwiseButtonEntity(coordinator, device_id)
28
+ for device_id in coordinator.data
29
+ if device_id == coordinator.api.gateway_id and coordinator.api.reboot
30
+ )
 
 
 
 
 
 
 
 
31
 
32
 
33
  class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
 
47
  self._attr_unique_id = f"{device_id}-reboot"
48
 
49
  @plugwise_command
50
+ @override
51
  async def async_press(self) -> None:
52
  """Triggers the Plugwise button press service."""
53
  await self.coordinator.api.reboot_gateway()
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py RENAMED
@@ -1,152 +1,121 @@
1
  """Plugwise Climate component for Home Assistant."""
2
 
3
- from __future__ import annotations
4
-
5
- from typing import Any
6
 
7
  from homeassistant.components.climate import (
8
  ATTR_HVAC_MODE,
9
  ATTR_TARGET_TEMP_HIGH,
10
  ATTR_TARGET_TEMP_LOW,
11
- PRESET_AWAY, # pw-beta homekit emulation
12
- PRESET_HOME, # pw-beta homekit emulation
13
  ClimateEntity,
14
  ClimateEntityFeature,
15
  HVACAction,
16
  HVACMode,
17
  )
18
- from homeassistant.const import (
19
- ATTR_NAME,
20
- ATTR_TEMPERATURE,
21
- STATE_OFF,
22
- STATE_ON,
23
- UnitOfTemperature,
24
- )
25
  from homeassistant.core import HomeAssistant, callback
26
  from homeassistant.exceptions import HomeAssistantError
27
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
28
 
29
- from . import PlugwiseConfigEntry
30
- from .const import (
31
- ACTIVE_PRESET,
32
- AVAILABLE_SCHEDULES,
33
- CLIMATE_MODE,
34
- CONF_HOMEKIT_EMULATION, # pw-beta homekit emulation
35
- CONTROL_STATE,
36
- COOLING_PRESENT,
37
- DEV_CLASS,
38
- DOMAIN,
39
- GATEWAY_ID,
40
- LOCATION,
41
- LOGGER,
42
- LOWER_BOUND,
43
- MASTER_THERMOSTATS,
44
- REGULATION_MODES,
45
- RESOLUTION,
46
- SELECT_REGULATION_MODE,
47
- SENSORS,
48
- SMILE_NAME,
49
- TARGET_TEMP,
50
- TARGET_TEMP_HIGH,
51
- TARGET_TEMP_LOW,
52
- THERMOSTAT,
53
- UPPER_BOUND,
54
- )
55
- from .coordinator import PlugwiseDataUpdateCoordinator
56
  from .entity import PlugwiseEntity
57
  from .util import plugwise_command
58
 
59
- PARALLEL_UPDATES = 0 # Upstream
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
 
62
  async def async_setup_entry(
63
  hass: HomeAssistant,
64
  entry: PlugwiseConfigEntry,
65
- async_add_entities: AddEntitiesCallback,
66
  ) -> None:
67
- """Set up Plugwise thermostats from a config entry."""
68
  coordinator = entry.runtime_data
69
- homekit_enabled: bool = entry.options.get(
70
- CONF_HOMEKIT_EMULATION, False
71
- ) # pw-beta homekit emulation
72
 
73
  @callback
74
  def _add_entities() -> None:
75
- """Add Entities during init and runtime."""
76
  if not coordinator.new_devices:
77
  return
78
 
79
- entities: list[PlugwiseClimateEntity] = []
80
- gateway_name = coordinator.data.gateway[SMILE_NAME]
81
- for device_id in coordinator.new_devices:
82
- device = coordinator.data.devices[device_id]
83
- if gateway_name == "Adam":
84
- if device[DEV_CLASS] == "climate":
85
- entities.append(
86
- PlugwiseClimateEntity(
87
- coordinator, device_id, homekit_enabled
88
- ) # pw-beta homekit emulation
89
- )
90
- LOGGER.debug("Add climate %s", device[ATTR_NAME])
91
- elif device[DEV_CLASS] in MASTER_THERMOSTATS:
92
- entities.append(
93
- PlugwiseClimateEntity(
94
- coordinator, device_id, homekit_enabled
95
- ) # pw-beta homekit emulation
96
- )
97
- LOGGER.debug("Add climate %s", device[ATTR_NAME])
98
-
99
- async_add_entities(entities)
100
 
101
  _add_entities()
102
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
103
 
104
 
105
- class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
106
  """Representation of a Plugwise thermostat."""
107
 
108
- _attr_has_entity_name = True
109
  _attr_name = None
110
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
111
  _attr_translation_key = DOMAIN
112
- _enable_turn_on_off_backwards_compatibility = False
113
-
114
- _previous_mode: str = HVACAction.HEATING # Upstream
115
- _homekit_mode: str | None = None # pw-beta homekit emulation + intentional unsort
116
 
117
  def __init__(
118
  self,
119
  coordinator: PlugwiseDataUpdateCoordinator,
120
  device_id: str,
121
- homekit_enabled: bool, # pw-beta homekit emulation
122
  ) -> None:
123
  """Set up the Plugwise API."""
124
  super().__init__(coordinator, device_id)
 
125
 
126
- self._devices = coordinator.data.devices
127
- self._gateway = coordinator.data.gateway
128
- gateway_id: str = self._gateway[GATEWAY_ID]
129
- self._gateway_data = self._devices[gateway_id]
130
- self._homekit_enabled = homekit_enabled # pw-beta homekit emulation
131
-
132
  self._location = device_id
133
- if (location := self.device.get(LOCATION)) is not None:
134
  self._location = location
135
-
136
- self._attr_max_temp = min(self.device[THERMOSTAT][UPPER_BOUND], 35.0)
137
- self._attr_min_temp = self.device[THERMOSTAT][LOWER_BOUND]
138
- # Ensure we don't drop below 0.1
139
- self._attr_target_temperature_step = max(
140
- self.device[THERMOSTAT][RESOLUTION], 0.1
141
- )
142
- self._attr_unique_id = f"{device_id}-climate"
143
 
144
  # Determine supported features
145
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
146
- if (
147
- self._gateway[COOLING_PRESENT]
148
- and self._gateway[SMILE_NAME] != "Adam"
149
- ):
150
  self._attr_supported_features = (
151
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
152
  )
@@ -154,86 +123,101 @@
154
  self._attr_supported_features |= (
155
  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
156
  )
157
- if presets := self.device["preset_modes"]: # can be NONE
158
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
159
- self._attr_preset_modes = presets
160
 
161
- def _previous_action_mode(self, coordinator: PlugwiseDataUpdateCoordinator) -> None:
162
- """Return the previous action-mode when the regulation-mode is not heating or cooling.
 
 
 
 
163
 
164
- Helper for set_hvac_mode().
165
- """
166
- # When no cooling available, _previous_mode is always heating
167
- if (
168
- REGULATION_MODES in self._gateway_data
169
- and HVACAction.COOLING in self._gateway_data[REGULATION_MODES]
170
- ):
171
- mode = self._gateway_data[SELECT_REGULATION_MODE]
172
- if mode in (HVACAction.COOLING, HVACAction.HEATING):
173
- self._previous_mode = mode
 
 
 
174
 
175
  @property
176
- def current_temperature(self) -> float:
 
 
 
 
 
 
 
 
 
 
177
  """Return the current temperature."""
178
- return self.device[SENSORS][ATTR_TEMPERATURE]
179
 
180
  @property
 
181
  def target_temperature(self) -> float:
182
  """Return the temperature we try to reach.
183
 
184
  Connected to the HVACMode combination of AUTO-HEAT.
185
  """
186
 
187
- return self.device[THERMOSTAT][TARGET_TEMP]
188
 
189
  @property
 
190
  def target_temperature_high(self) -> float:
191
  """Return the temperature we try to reach in case of cooling.
192
 
193
  Connected to the HVACMode combination of AUTO-HEAT_COOL.
194
  """
195
- return self.device[THERMOSTAT][TARGET_TEMP_HIGH]
196
 
197
  @property
 
198
  def target_temperature_low(self) -> float:
199
  """Return the heating temperature we try to reach in case of heating.
200
 
201
  Connected to the HVACMode combination AUTO-HEAT_COOL.
202
  """
203
- return self.device[THERMOSTAT][TARGET_TEMP_LOW]
204
 
205
  @property
 
206
  def hvac_mode(self) -> HVACMode:
207
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
208
  if (
209
- mode := self.device[CLIMATE_MODE]
210
- ) is None or mode not in self.hvac_modes: # pw-beta add to Core
211
- return HVACMode.HEAT # pragma: no cover
212
- # pw-beta homekit emulation
213
- if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
214
- mode = HVACMode.OFF # pragma: no cover
215
-
216
  return HVACMode(mode)
217
 
218
  @property
 
219
  def hvac_modes(self) -> list[HVACMode]:
220
  """Return a list of available HVACModes."""
221
  hvac_modes: list[HVACMode] = []
222
- if (
223
- self._homekit_enabled # pw-beta homekit emulation
224
- or REGULATION_MODES in self._gateway_data
225
- ):
226
  hvac_modes.append(HVACMode.OFF)
227
 
228
- if AVAILABLE_SCHEDULES in self.device:
229
  hvac_modes.append(HVACMode.AUTO)
230
 
231
- if self._gateway[COOLING_PRESENT]:
232
- if REGULATION_MODES in self._gateway_data:
233
- if self._gateway_data[SELECT_REGULATION_MODE] == HVACAction.COOLING:
234
- hvac_modes.append(HVACMode.COOL)
235
- if self._gateway_data[SELECT_REGULATION_MODE] == HVACAction.HEATING:
236
  hvac_modes.append(HVACMode.HEAT)
 
 
237
  else:
238
  hvac_modes.append(HVACMode.HEAT_COOL)
239
  else:
@@ -242,72 +226,110 @@
242
  return hvac_modes
243
 
244
  @property
245
- def hvac_action(self) -> HVACAction: # pw-beta add to Core
 
246
  """Return the current running hvac operation if supported."""
247
- # Keep track of the previous action-mode
248
- self._previous_action_mode(self.coordinator)
 
 
 
 
 
 
 
249
 
250
- if (action := self.device.get(CONTROL_STATE)) is not None:
251
  return HVACAction(action)
252
 
253
  return HVACAction.IDLE
254
 
255
  @property
 
256
  def preset_mode(self) -> str | None:
257
  """Return the current preset mode."""
258
- return self.device[ACTIVE_PRESET]
259
 
260
  @plugwise_command
 
261
  async def async_set_temperature(self, **kwargs: Any) -> None:
262
  """Set new target temperature."""
263
  data: dict[str, Any] = {}
264
  if ATTR_TEMPERATURE in kwargs:
265
- data[TARGET_TEMP] = kwargs.get(ATTR_TEMPERATURE)
266
  if ATTR_TARGET_TEMP_HIGH in kwargs:
267
- data[TARGET_TEMP_HIGH] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
268
  if ATTR_TARGET_TEMP_LOW in kwargs:
269
- data[TARGET_TEMP_LOW] = kwargs.get(ATTR_TARGET_TEMP_LOW)
270
-
271
- # Upstream removed input-valid check
272
 
273
  if mode := kwargs.get(ATTR_HVAC_MODE):
274
  await self.async_set_hvac_mode(mode)
275
 
276
- await self.coordinator.api.set_temperature(self._location, data)
 
 
 
 
 
 
 
 
 
 
 
277
 
278
  @plugwise_command
 
279
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
280
- """Set the hvac mode."""
281
- if hvac_mode not in self.hvac_modes:
282
- raise HomeAssistantError("Unsupported hvac_mode")
283
-
284
  if hvac_mode == self.hvac_mode:
285
  return
286
 
287
- if hvac_mode != HVACMode.OFF:
288
- await self.coordinator.api.set_schedule_state(
289
- self._location,
290
- STATE_ON if hvac_mode == HVACMode.AUTO else STATE_OFF,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  )
 
 
292
 
293
- if (
294
- not self._homekit_enabled
295
- ): # pw-beta: feature request - mimic HomeKit behavior
296
- if hvac_mode == HVACMode.OFF:
297
- await self.coordinator.api.set_regulation_mode(hvac_mode)
298
- elif self.hvac_mode == HVACMode.OFF:
299
- await self.coordinator.api.set_regulation_mode(self._previous_mode)
300
- else:
301
- self._homekit_mode = hvac_mode # pragma: no cover
302
- if self._homekit_mode == HVACMode.OFF: # pragma: no cover
303
- await self.async_set_preset_mode(PRESET_AWAY) # pragma: no cover
304
- if (
305
- self._homekit_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]
306
- and self.device[ACTIVE_PRESET] == PRESET_AWAY
307
- ): # pragma: no cover
308
- await self.async_set_preset_mode(PRESET_HOME) # pragma: no cover
309
 
310
  @plugwise_command
 
311
  async def async_set_preset_mode(self, preset_mode: str) -> None:
312
  """Set the preset mode."""
313
- await self.coordinator.api.set_preset(self._location, preset_mode)
 
1
  """Plugwise Climate component for Home Assistant."""
2
 
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Any, override
 
5
 
6
  from homeassistant.components.climate import (
7
  ATTR_HVAC_MODE,
8
  ATTR_TARGET_TEMP_HIGH,
9
  ATTR_TARGET_TEMP_LOW,
 
 
10
  ClimateEntity,
11
  ClimateEntityFeature,
12
  HVACAction,
13
  HVACMode,
14
  )
15
+ from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON, UnitOfTemperature
 
 
 
 
 
 
16
  from homeassistant.core import HomeAssistant, callback
17
  from homeassistant.exceptions import HomeAssistantError
18
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
19
+ from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
20
 
21
+ from .const import DOMAIN, MASTER_THERMOSTATS
22
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  from .entity import PlugwiseEntity
24
  from .util import plugwise_command
25
 
26
+ ERROR_NO_SCHEDULE = "set_schedule_first"
27
+ PARALLEL_UPDATES = 0
28
+
29
+
30
+ def _check_for_schedule(active: bool, last_active: str | None) -> None:
31
+ """Raise a HAError when no thermostat schedule has been set."""
32
+ if not active and last_active is None:
33
+ raise HomeAssistantError(
34
+ translation_domain=DOMAIN,
35
+ translation_key=ERROR_NO_SCHEDULE,
36
+ )
37
+
38
+
39
+ @dataclass
40
+ class PlugwiseClimateExtraStoredData(ExtraStoredData):
41
+ """Object to hold extra stored data."""
42
+
43
+ last_active_schedule: str | None
44
+ previous_action_mode: str | None
45
+
46
+ @override
47
+ def as_dict(self) -> dict[str, Any]:
48
+ """Return a dict representation of the text data."""
49
+ return asdict(self)
50
+
51
+ @classmethod
52
+ def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData:
53
+ """Initialize a stored data object from a dict."""
54
+ return cls(
55
+ last_active_schedule=restored.get("last_active_schedule"),
56
+ previous_action_mode=restored.get("previous_action_mode"),
57
+ )
58
 
59
 
60
  async def async_setup_entry(
61
  hass: HomeAssistant,
62
  entry: PlugwiseConfigEntry,
63
+ async_add_entities: AddConfigEntryEntitiesCallback,
64
  ) -> None:
65
+ """Set up the Smile Thermostats from a config entry."""
66
  coordinator = entry.runtime_data
 
 
 
67
 
68
  @callback
69
  def _add_entities() -> None:
70
+ """Add Entities."""
71
  if not coordinator.new_devices:
72
  return
73
 
74
+ if coordinator.api.smile.name == "Adam":
75
+ async_add_entities(
76
+ PlugwiseClimateEntity(coordinator, device_id)
77
+ for device_id in coordinator.new_devices
78
+ if coordinator.data[device_id]["dev_class"] == "climate"
79
+ )
80
+ else:
81
+ async_add_entities(
82
+ PlugwiseClimateEntity(coordinator, device_id)
83
+ for device_id in coordinator.new_devices
84
+ if coordinator.data[device_id]["dev_class"] in MASTER_THERMOSTATS
85
+ )
 
 
 
 
 
 
 
 
 
86
 
87
  _add_entities()
88
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
89
 
90
 
91
+ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
92
  """Representation of a Plugwise thermostat."""
93
 
 
94
  _attr_name = None
95
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
96
  _attr_translation_key = DOMAIN
 
 
 
 
97
 
98
  def __init__(
99
  self,
100
  coordinator: PlugwiseDataUpdateCoordinator,
101
  device_id: str,
 
102
  ) -> None:
103
  """Set up the Plugwise API."""
104
  super().__init__(coordinator, device_id)
105
+ self._attr_unique_id = f"{device_id}-climate" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
106
 
107
+ self._api = coordinator.api
108
+ gateway_id: str = self._api.gateway_id
109
+ self._gateway_data = coordinator.data[gateway_id]
110
+ self._last_active_schedule: str | None = None
 
 
111
  self._location = device_id
112
+ if (location := self.device.get("location")) is not None:
113
  self._location = location
114
+ self._previous_action_mode = HVACAction.HEATING.value
 
 
 
 
 
 
 
115
 
116
  # Determine supported features
117
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
118
+ if self._api.cooling_present and self._api.smile.name != "Adam":
 
 
 
119
  self._attr_supported_features = (
120
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
121
  )
 
123
  self._attr_supported_features |= (
124
  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
125
  )
126
+ if presets := self.device.get("preset_modes"):
127
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
128
+ self._attr_preset_modes = presets
129
 
130
+ self._attr_min_temp = self.device["thermostat"]["lower_bound"]
131
+ self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
132
+ # Ensure we don't drop below 0.1
133
+ self._attr_target_temperature_step = max(
134
+ self.device["thermostat"]["resolution"], 0.1
135
+ )
136
 
137
+ @override
138
+ async def async_added_to_hass(self) -> None:
139
+ """Run when entity about to be added."""
140
+ await super().async_added_to_hass()
141
+
142
+ if extra_data := await self.async_get_last_extra_data():
143
+ plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
144
+ extra_data.as_dict()
145
+ )
146
+ self._last_active_schedule = plugwise_extra_data.last_active_schedule
147
+ self._previous_action_mode = (
148
+ plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value
149
+ )
150
 
151
  @property
152
+ @override
153
+ def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
154
+ """Return text specific state data to be restored."""
155
+ return PlugwiseClimateExtraStoredData(
156
+ last_active_schedule=self._last_active_schedule,
157
+ previous_action_mode=self._previous_action_mode,
158
+ )
159
+
160
+ @property
161
+ @override
162
+ def current_temperature(self) -> float | None:
163
  """Return the current temperature."""
164
+ return self.device["sensors"].get("temperature")
165
 
166
  @property
167
+ @override
168
  def target_temperature(self) -> float:
169
  """Return the temperature we try to reach.
170
 
171
  Connected to the HVACMode combination of AUTO-HEAT.
172
  """
173
 
174
+ return self.device["thermostat"]["setpoint"]
175
 
176
  @property
177
+ @override
178
  def target_temperature_high(self) -> float:
179
  """Return the temperature we try to reach in case of cooling.
180
 
181
  Connected to the HVACMode combination of AUTO-HEAT_COOL.
182
  """
183
+ return self.device["thermostat"]["setpoint_high"]
184
 
185
  @property
186
+ @override
187
  def target_temperature_low(self) -> float:
188
  """Return the heating temperature we try to reach in case of heating.
189
 
190
  Connected to the HVACMode combination AUTO-HEAT_COOL.
191
  """
192
+ return self.device["thermostat"]["setpoint_low"]
193
 
194
  @property
195
+ @override
196
  def hvac_mode(self) -> HVACMode:
197
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
198
  if (
199
+ mode := self.device.get("climate_mode")
200
+ ) is None or mode not in self.hvac_modes:
201
+ return HVACMode.HEAT
 
 
 
 
202
  return HVACMode(mode)
203
 
204
  @property
205
+ @override
206
  def hvac_modes(self) -> list[HVACMode]:
207
  """Return a list of available HVACModes."""
208
  hvac_modes: list[HVACMode] = []
209
+ if "regulation_modes" in self._gateway_data:
 
 
 
210
  hvac_modes.append(HVACMode.OFF)
211
 
212
+ if self.device.get("available_schedules"):
213
  hvac_modes.append(HVACMode.AUTO)
214
 
215
+ if self._api.cooling_present:
216
+ if "regulation_modes" in self._gateway_data:
217
+ if "heating" in self._gateway_data["regulation_modes"]:
 
 
218
  hvac_modes.append(HVACMode.HEAT)
219
+ if "cooling" in self._gateway_data["regulation_modes"]:
220
+ hvac_modes.append(HVACMode.COOL)
221
  else:
222
  hvac_modes.append(HVACMode.HEAT_COOL)
223
  else:
 
226
  return hvac_modes
227
 
228
  @property
229
+ @override
230
+ def hvac_action(self) -> HVACAction:
231
  """Return the current running hvac operation if supported."""
232
+ # Keep track of the previous hvac_action mode.
233
+ # When no cooling available, _previous_action_mode is always heating
234
+ if (
235
+ "regulation_modes" in self._gateway_data
236
+ and HVACAction.COOLING.value in self._gateway_data["regulation_modes"]
237
+ ):
238
+ mode = self._gateway_data["select_regulation_mode"]
239
+ if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
240
+ self._previous_action_mode = mode
241
 
242
+ if (action := self.device.get("control_state")) is not None:
243
  return HVACAction(action)
244
 
245
  return HVACAction.IDLE
246
 
247
  @property
248
+ @override
249
  def preset_mode(self) -> str | None:
250
  """Return the current preset mode."""
251
+ return self.device.get("active_preset")
252
 
253
  @plugwise_command
254
+ @override
255
  async def async_set_temperature(self, **kwargs: Any) -> None:
256
  """Set new target temperature."""
257
  data: dict[str, Any] = {}
258
  if ATTR_TEMPERATURE in kwargs:
259
+ data["setpoint"] = kwargs.get(ATTR_TEMPERATURE)
260
  if ATTR_TARGET_TEMP_HIGH in kwargs:
261
+ data["setpoint_high"] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
262
  if ATTR_TARGET_TEMP_LOW in kwargs:
263
+ data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
 
 
264
 
265
  if mode := kwargs.get(ATTR_HVAC_MODE):
266
  await self.async_set_hvac_mode(mode)
267
 
268
+ await self._api.set_temperature(self._location, data)
269
+
270
+ def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
271
+ """Return the API regulation value for a manual HVAC mode, or None.
272
+
273
+ The function inputs are limited to the HVACModes HEAT and COOL.
274
+ """
275
+ if hvac_mode == HVACMode.HEAT:
276
+ mode = HVACAction.HEATING.value
277
+ if hvac_mode == HVACMode.COOL:
278
+ mode = HVACAction.COOLING.value
279
+ return mode
280
 
281
  @plugwise_command
282
+ @override
283
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
284
+ """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
285
+ # Early exit if no mode change
 
 
286
  if hvac_mode == self.hvac_mode:
287
  return
288
 
289
+ # Adam only: set to HVACMode.OFF
290
+ if hvac_mode == HVACMode.OFF:
291
+ await self._api.set_regulation_mode(hvac_mode.value)
292
+ return
293
+
294
+ current_schedule = self.device.get("select_schedule")
295
+ schedule_is_active = current_schedule not in (None, "off")
296
+ desired_schedule = (
297
+ current_schedule if schedule_is_active else self._last_active_schedule
298
+ )
299
+ # Adam only: transition from HVACMode.OFF
300
+ if self.hvac_mode == HVACMode.OFF:
301
+ if hvac_mode == HVACMode.AUTO:
302
+ _check_for_schedule(schedule_is_active, self._last_active_schedule)
303
+ await self._api.set_schedule_state(
304
+ self._location, STATE_ON, desired_schedule
305
+ )
306
+ await self._api.set_regulation_mode(self._previous_action_mode)
307
+ return
308
+
309
+ # Transition to manual mode
310
+ if schedule_is_active:
311
+ await self._api.set_schedule_state(
312
+ self._location, STATE_OFF, current_schedule
313
+ )
314
+ self._last_active_schedule = current_schedule
315
+ regulation = self._regulation_mode_for_hvac(hvac_mode)
316
+ await self._api.set_regulation_mode(regulation)
317
+ return
318
+
319
+ # Common - transition from auto = schedule off
320
+ if self.hvac_mode == HVACMode.AUTO:
321
+ await self._api.set_schedule_state(
322
+ self._location, STATE_OFF, current_schedule
323
  )
324
+ self._last_active_schedule = current_schedule
325
+ return
326
 
327
+ # Common - transition to auto = schedule on
328
+ _check_for_schedule(schedule_is_active, self._last_active_schedule)
329
+ await self._api.set_schedule_state(self._location, STATE_ON, desired_schedule)
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
  @plugwise_command
332
+ @override
333
  async def async_set_preset_mode(self, preset_mode: str) -> None:
334
  """Set the preset mode."""
335
+ await self._api.set_preset(self._location, preset_mode)
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py RENAMED
@@ -1,9 +1,7 @@
1
  """Config flow for Plugwise integration."""
2
 
3
- from __future__ import annotations
4
-
5
- from copy import deepcopy
6
- from typing import Any, Self
7
 
8
  from plugwise import Smile
9
  from plugwise.exceptions import (
@@ -16,16 +14,7 @@
16
  )
17
  import voluptuous as vol
18
 
19
- from homeassistant.components.zeroconf import ZeroconfServiceInfo
20
- from homeassistant.config_entries import (
21
- SOURCE_USER,
22
- ConfigEntry,
23
- ConfigFlow,
24
- ConfigFlowResult,
25
- OptionsFlow,
26
- )
27
-
28
- # Upstream
29
  from homeassistant.const import (
30
  ATTR_CONFIGURATION_URL,
31
  CONF_BASE,
@@ -33,80 +22,58 @@
33
  CONF_NAME,
34
  CONF_PASSWORD,
35
  CONF_PORT,
36
- CONF_SCAN_INTERVAL,
37
  CONF_USERNAME,
38
  )
39
-
40
- # Upstream
41
- from homeassistant.core import HomeAssistant, callback
42
- from homeassistant.helpers import config_validation as cv
43
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
 
44
 
45
  from .const import (
46
  ANNA_WITH_ADAM,
47
- CONF_HOMEKIT_EMULATION, # pw-beta option
48
- CONF_REFRESH_INTERVAL, # pw-beta option
49
  DEFAULT_PORT,
50
- DEFAULT_SCAN_INTERVAL, # pw-beta option
51
  DEFAULT_USERNAME,
52
  DOMAIN,
53
  FLOW_SMILE,
54
  FLOW_STRETCH,
55
- INIT,
56
  SMILE,
57
  SMILE_OPEN_THERM,
58
  SMILE_THERMO,
59
  STRETCH,
60
  STRETCH_USERNAME,
61
- THERMOSTAT,
62
- TITLE_PLACEHOLDERS,
63
- VERSION,
64
  ZEROCONF_MAP,
65
  )
66
 
67
- # Upstream
68
- from .coordinator import PlugwiseDataUpdateCoordinator
69
-
70
- type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
71
 
72
- # Upstream basically the whole file (excluding the pw-beta options)
 
 
 
 
73
 
74
 
75
- def base_schema(
76
- cf_input: ZeroconfServiceInfo | dict[str, Any] | None,
77
- ) -> vol.Schema:
78
  """Generate base schema for gateways."""
79
- if not cf_input: # no discovery- or user-input available
80
- return vol.Schema(
 
 
81
  {
82
  vol.Required(CONF_HOST): str,
83
- vol.Required(CONF_PASSWORD): str,
84
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
85
  vol.Required(CONF_USERNAME, default=SMILE): vol.In(
86
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
87
  ),
88
  }
89
  )
90
 
91
- if isinstance(cf_input, ZeroconfServiceInfo):
92
- return vol.Schema({vol.Required(CONF_PASSWORD): str})
93
-
94
- return vol.Schema(
95
- {
96
- vol.Required(CONF_HOST, default=cf_input[CONF_HOST]): str,
97
- vol.Required(CONF_PASSWORD, default=cf_input[CONF_PASSWORD]): str,
98
- vol.Optional(CONF_PORT, default=cf_input[CONF_PORT]): int,
99
- vol.Required(CONF_USERNAME, default=cf_input[CONF_USERNAME]): vol.In(
100
- {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
101
- ),
102
- }
103
- )
104
 
105
 
106
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
107
  """Validate whether the user input allows us to connect to the gateway.
108
 
109
- Data has the keys from base_schema() with values provided by the user.
110
  """
111
  websession = async_get_clientsession(hass, verify_ssl=False)
112
  api = Smile(
@@ -120,37 +87,59 @@
120
  return api
121
 
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
124
  """Handle a config flow for Plugwise Smile."""
125
 
126
  VERSION = 1
127
- MINOR_VERSION = 1
128
 
129
  discovery_info: ZeroconfServiceInfo | None = None
130
- product: str = "Unknown Smile"
131
  _username: str = DEFAULT_USERNAME
132
 
 
133
  async def async_step_zeroconf(
134
  self, discovery_info: ZeroconfServiceInfo
135
  ) -> ConfigFlowResult:
136
  """Prepare configuration for a discovered Plugwise Smile."""
137
  self.discovery_info = discovery_info
138
  _properties = discovery_info.properties
139
- _version = _properties.get(VERSION, "n/a")
140
- self.product = _product = _properties.get("product", "Unknown Smile")
141
- unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
142
- if DEFAULT_USERNAME not in unique_id:
143
- self._username = STRETCH_USERNAME
144
 
 
145
  if config_entry := await self.async_set_unique_id(unique_id):
146
  try:
147
  await validate_input(
148
  self.hass,
149
  {
150
  CONF_HOST: discovery_info.host,
151
- CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
152
  CONF_PORT: discovery_info.port,
153
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
 
154
  },
155
  )
156
  except Exception: # noqa: BLE001
@@ -163,6 +152,12 @@
163
  }
164
  )
165
 
 
 
 
 
 
 
166
  # This is an Anna, but we already have config entries.
167
  # Assuming that the user has already configured Adam, aborting discovery.
168
  if self._async_current_entries() and _product == SMILE_THERMO:
@@ -172,19 +167,19 @@
172
  # In that case, we need to cancel the Anna flow, as the Adam should
173
  # be added.
174
  if self.hass.config_entries.flow.async_has_matching_flow(self):
175
- return self.async_abort(reason="anna_with_adam")
176
 
177
- _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
178
  self.context.update(
179
  {
180
- TITLE_PLACEHOLDERS: {CONF_NAME: _name},
181
  ATTR_CONFIGURATION_URL: (
182
  f"http://{discovery_info.host}:{discovery_info.port}"
183
- )
184
  }
185
  )
186
  return await self.async_step_user()
187
 
 
188
  def is_matching(self, other_flow: Self) -> bool:
189
  """Return True if other_flow is matching this flow."""
190
  # This is an Anna, and there is already an Adam flow in progress
@@ -197,113 +192,70 @@
197
 
198
  return False
199
 
 
200
  async def async_step_user(
201
  self, user_input: dict[str, Any] | None = None
202
  ) -> ConfigFlowResult:
203
  """Handle the initial step when using network/gateway setups."""
204
  errors: dict[str, str] = {}
205
 
206
- if not user_input:
207
- return self.async_show_form(
208
- step_id=SOURCE_USER,
209
- data_schema=base_schema(self.discovery_info),
210
- errors=errors,
211
- )
212
-
213
- if self.discovery_info:
214
- user_input[CONF_HOST] = self.discovery_info.host
215
- user_input[CONF_PORT] = self.discovery_info.port
216
- user_input[CONF_USERNAME] = self._username
217
-
218
- try:
219
- api = await validate_input(self.hass, user_input)
220
- except ConnectionFailedError:
221
- errors[CONF_BASE] = "cannot_connect"
222
- except InvalidAuthentication:
223
- errors[CONF_BASE] = "invalid_auth"
224
- except InvalidSetupError:
225
- errors[CONF_BASE] = "invalid_setup"
226
- except (InvalidXMLError, ResponseError):
227
- errors[CONF_BASE] = "response_error"
228
- except UnsupportedDeviceError:
229
- errors[CONF_BASE] = "unsupported"
230
- except Exception: # noqa: BLE001
231
- errors[CONF_BASE] = "unknown"
232
-
233
- if errors:
234
- return self.async_show_form(
235
- step_id=SOURCE_USER,
236
- data_schema=base_schema(user_input),
237
- errors=errors,
238
- )
239
 
240
- await self.async_set_unique_id(
241
- api.smile_hostname or api.gateway_id, raise_on_progress=False
 
 
242
  )
243
- self._abort_if_unique_id_configured()
244
- return self.async_create_entry(title=api.smile_name, data=user_input)
245
 
246
- @staticmethod
247
- @callback
248
- def async_get_options_flow(
249
- config_entry: PlugwiseConfigEntry,
250
- ) -> PlugwiseOptionsFlowHandler: # pw-beta options
251
- """Get the options flow for this handler."""
252
- return PlugwiseOptionsFlowHandler(config_entry)
253
-
254
-
255
- # pw-beta - change the scan-interval via CONFIGURE
256
- # pw-beta - add homekit emulation via CONFIGURE
257
- # pw-beta - change the frontend refresh interval via CONFIGURE
258
- class PlugwiseOptionsFlowHandler(OptionsFlow): # pw-beta options
259
- """Plugwise option flow."""
260
-
261
- def __init__(self, config_entry: ConfigEntry) -> None:
262
- """Initialize options flow."""
263
- self.options = deepcopy(dict(config_entry.options))
264
-
265
- def _create_options_schema(self, coordinator: PlugwiseDataUpdateCoordinator) -> vol.Schema:
266
- interval = DEFAULT_SCAN_INTERVAL[coordinator.api.smile_type] # pw-beta options
267
- schema = {
268
- vol.Optional(
269
- CONF_SCAN_INTERVAL,
270
- default=self.options.get(CONF_SCAN_INTERVAL, interval.seconds),
271
- ): vol.All(cv.positive_int, vol.Clamp(min=10)),
272
- } # pw-beta
273
-
274
- if coordinator.api.smile_type == THERMOSTAT:
275
- schema.update({
276
- vol.Optional(
277
- CONF_HOMEKIT_EMULATION,
278
- default=self.options.get(CONF_HOMEKIT_EMULATION, False),
279
- ): vol.All(cv.boolean),
280
- vol.Optional(
281
- CONF_REFRESH_INTERVAL,
282
- default=self.options.get(CONF_REFRESH_INTERVAL, 1.5),
283
- ): vol.All(vol.Coerce(float), vol.Range(min=1.5, max=10.0)),
284
- }) # pw-beta
285
 
286
- return vol.Schema(schema)
287
 
288
- async def async_step_none(
289
- self, user_input: dict[str, Any] | None = None
290
- ) -> ConfigFlowResult: # pragma: no cover
291
- """No options available."""
292
- if user_input is not None:
293
- # Apparently not possible to abort an options flow at the moment
294
- return self.async_create_entry(title="", data=self.options)
295
- return self.async_show_form(step_id="none")
296
-
297
- async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
298
- """Manage the Plugwise options."""
299
- if not self.config_entry.data.get(CONF_HOST):
300
- return await self.async_step_none(user_input) # pragma: no cover
301
 
302
- if user_input is not None:
303
- return self.async_create_entry(title="", data=user_input)
 
 
 
 
 
 
 
 
 
304
 
305
- coordinator = self.config_entry.runtime_data
306
  return self.async_show_form(
307
- step_id=INIT,
308
- data_schema=self._create_options_schema(coordinator)
 
 
 
 
 
309
  )
 
1
  """Config flow for Plugwise integration."""
2
 
3
+ import logging
4
+ from typing import Any, Self, override
 
 
5
 
6
  from plugwise import Smile
7
  from plugwise.exceptions import (
 
14
  )
15
  import voluptuous as vol
16
 
17
+ from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
 
 
 
 
 
 
 
 
 
18
  from homeassistant.const import (
19
  ATTR_CONFIGURATION_URL,
20
  CONF_BASE,
 
22
  CONF_NAME,
23
  CONF_PASSWORD,
24
  CONF_PORT,
 
25
  CONF_USERNAME,
26
  )
27
+ from homeassistant.core import HomeAssistant
 
 
 
28
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
29
+ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
30
 
31
  from .const import (
32
  ANNA_WITH_ADAM,
 
 
33
  DEFAULT_PORT,
 
34
  DEFAULT_USERNAME,
35
  DOMAIN,
36
  FLOW_SMILE,
37
  FLOW_STRETCH,
 
38
  SMILE,
39
  SMILE_OPEN_THERM,
40
  SMILE_THERMO,
41
  STRETCH,
42
  STRETCH_USERNAME,
43
+ UNKNOWN_SMILE,
 
 
44
  ZEROCONF_MAP,
45
  )
46
 
47
+ _LOGGER = logging.getLogger(__name__)
 
 
 
48
 
49
+ SMILE_RECONF_SCHEMA = vol.Schema(
50
+ {
51
+ vol.Required(CONF_HOST): str,
52
+ }
53
+ )
54
 
55
 
56
+ def smile_user_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
 
 
57
  """Generate base schema for gateways."""
58
+ schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
59
+
60
+ if not discovery_info:
61
+ schema = schema.extend(
62
  {
63
  vol.Required(CONF_HOST): str,
 
 
64
  vol.Required(CONF_USERNAME, default=SMILE): vol.In(
65
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
66
  ),
67
  }
68
  )
69
 
70
+ return schema
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
74
  """Validate whether the user input allows us to connect to the gateway.
75
 
76
+ Data has the keys from the schema with values provided by the user.
77
  """
78
  websession = async_get_clientsession(hass, verify_ssl=False)
79
  api = Smile(
 
87
  return api
88
 
89
 
90
+ async def verify_connection(
91
+ hass: HomeAssistant, user_input: dict[str, Any]
92
+ ) -> tuple[Smile | None, dict[str, str]]:
93
+ """Verify and return the gateway connection or an error."""
94
+ errors: dict[str, str] = {}
95
+
96
+ try:
97
+ return (await validate_input(hass, user_input), errors)
98
+ except ConnectionFailedError:
99
+ errors[CONF_BASE] = "cannot_connect"
100
+ except InvalidAuthentication:
101
+ errors[CONF_BASE] = "invalid_auth"
102
+ except InvalidSetupError:
103
+ errors[CONF_BASE] = "invalid_setup"
104
+ except InvalidXMLError, ResponseError:
105
+ errors[CONF_BASE] = "response_error"
106
+ except UnsupportedDeviceError:
107
+ errors[CONF_BASE] = "unsupported"
108
+ except Exception:
109
+ _LOGGER.exception(
110
+ "Unknown exception while verifying connection with your Plugwise Smile"
111
+ )
112
+ errors[CONF_BASE] = "unknown"
113
+ return (None, errors)
114
+
115
+
116
  class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
117
  """Handle a config flow for Plugwise Smile."""
118
 
119
  VERSION = 1
 
120
 
121
  discovery_info: ZeroconfServiceInfo | None = None
122
+ product: str = UNKNOWN_SMILE
123
  _username: str = DEFAULT_USERNAME
124
 
125
+ @override
126
  async def async_step_zeroconf(
127
  self, discovery_info: ZeroconfServiceInfo
128
  ) -> ConfigFlowResult:
129
  """Prepare configuration for a discovered Plugwise Smile."""
130
  self.discovery_info = discovery_info
131
  _properties = discovery_info.properties
 
 
 
 
 
132
 
133
+ unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
134
  if config_entry := await self.async_set_unique_id(unique_id):
135
  try:
136
  await validate_input(
137
  self.hass,
138
  {
139
  CONF_HOST: discovery_info.host,
 
140
  CONF_PORT: discovery_info.port,
141
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
142
+ CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
143
  },
144
  )
145
  except Exception: # noqa: BLE001
 
152
  }
153
  )
154
 
155
+ if DEFAULT_USERNAME not in unique_id:
156
+ self._username = STRETCH_USERNAME
157
+ self.product = _product = _properties.get("product", UNKNOWN_SMILE)
158
+ _version = _properties.get("version", "n/a")
159
+ _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
160
+
161
  # This is an Anna, but we already have config entries.
162
  # Assuming that the user has already configured Adam, aborting discovery.
163
  if self._async_current_entries() and _product == SMILE_THERMO:
 
167
  # In that case, we need to cancel the Anna flow, as the Adam should
168
  # be added.
169
  if self.hass.config_entries.flow.async_has_matching_flow(self):
170
+ return self.async_abort(reason=ANNA_WITH_ADAM)
171
 
 
172
  self.context.update(
173
  {
174
+ "title_placeholders": {CONF_NAME: _name},
175
  ATTR_CONFIGURATION_URL: (
176
  f"http://{discovery_info.host}:{discovery_info.port}"
177
+ ),
178
  }
179
  )
180
  return await self.async_step_user()
181
 
182
+ @override
183
  def is_matching(self, other_flow: Self) -> bool:
184
  """Return True if other_flow is matching this flow."""
185
  # This is an Anna, and there is already an Adam flow in progress
 
192
 
193
  return False
194
 
195
+ @override
196
  async def async_step_user(
197
  self, user_input: dict[str, Any] | None = None
198
  ) -> ConfigFlowResult:
199
  """Handle the initial step when using network/gateway setups."""
200
  errors: dict[str, str] = {}
201
 
202
+ if user_input is not None:
203
+ user_input[CONF_PORT] = DEFAULT_PORT
204
+ if self.discovery_info:
205
+ user_input[CONF_HOST] = self.discovery_info.host
206
+ user_input[CONF_PORT] = self.discovery_info.port
207
+ user_input[CONF_USERNAME] = self._username
208
+
209
+ api, errors = await verify_connection(self.hass, user_input)
210
+ if api:
211
+ await self.async_set_unique_id(
212
+ api.smile.hostname or api.gateway_id,
213
+ raise_on_progress=False,
214
+ )
215
+ self._abort_if_unique_id_configured()
216
+ return self.async_create_entry(title=api.smile.name, data=user_input)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
+ return self.async_show_form(
219
+ step_id=SOURCE_USER,
220
+ data_schema=smile_user_schema(self.discovery_info),
221
+ errors=errors,
222
  )
 
 
223
 
224
+ async def async_step_reconfigure(
225
+ self, user_input: dict[str, Any] | None = None
226
+ ) -> ConfigFlowResult:
227
+ """Handle reconfiguration of the integration."""
228
+ errors: dict[str, str] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
+ reconfigure_entry = self._get_reconfigure_entry()
231
 
232
+ if user_input:
233
+ # Keep current username and password
234
+ full_input = {
235
+ CONF_HOST: user_input.get(CONF_HOST),
236
+ CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
237
+ CONF_USERNAME: reconfigure_entry.data.get(CONF_USERNAME),
238
+ CONF_PASSWORD: reconfigure_entry.data.get(CONF_PASSWORD),
239
+ }
 
 
 
 
 
240
 
241
+ api, errors = await verify_connection(self.hass, full_input)
242
+ if api:
243
+ await self.async_set_unique_id(
244
+ api.smile.hostname or api.gateway_id,
245
+ raise_on_progress=False,
246
+ )
247
+ self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
248
+ return self.async_update_reload_and_abort(
249
+ reconfigure_entry,
250
+ data_updates=full_input,
251
+ )
252
 
 
253
  return self.async_show_form(
254
+ step_id="reconfigure",
255
+ data_schema=self.add_suggested_values_to_schema(
256
+ data_schema=SMILE_RECONF_SCHEMA,
257
+ suggested_values=reconfigure_entry.data,
258
+ ),
259
+ description_placeholders={"title": reconfigure_entry.title},
260
+ errors=errors,
261
  )
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py RENAMED
@@ -6,171 +6,28 @@
6
 
7
  from homeassistant.const import Platform
8
 
9
- # Upstream basically the whole file excluding pw-beta options
10
-
11
  DOMAIN: Final = "plugwise"
12
 
13
  LOGGER = logging.getLogger(__package__)
14
 
 
15
  API: Final = "api"
16
- COORDINATOR: Final = "coordinator"
17
- CONF_HOMEKIT_EMULATION: Final = "homekit_emulation" # pw-beta options
18
- CONF_REFRESH_INTERVAL: Final = "refresh_interval" # pw-beta options
19
- CONF_MANUAL_PATH: Final = "Enter Manually"
 
20
  GATEWAY: Final = "gateway"
21
  LOCATION: Final = "location"
22
- MAC_ADDRESS: Final = "mac_address"
23
  REBOOT: Final = "reboot"
24
  SMILE: Final = "smile"
25
- STRETCH: Final = "stretch"
26
- STRETCH_USERNAME: Final = "stretch"
27
- UNIQUE_IDS: Final = "unique_ids"
28
- ZIGBEE_MAC_ADDRESS: Final = "zigbee_mac_address"
29
-
30
- # Binary Sensor constants
31
- BINARY_SENSORS: Final = "binary_sensors"
32
- BATTERY_STATE: Final = "low_battery"
33
- COMPRESSOR_STATE: Final = "compressor_state"
34
- COOLING_ENABLED: Final = "cooling_enabled"
35
- COOLING_STATE: Final = "cooling_state"
36
- DHW_STATE: Final = "dhw_state"
37
- FLAME_STATE: Final = "flame_state"
38
- HEATING_STATE: Final = "heating_state"
39
- NOTIFICATIONS: Final ="notifications"
40
- PLUGWISE_NOTIFICATION: Final = "plugwise_notification"
41
- SECONDARY_BOILER_STATE: Final = "secondary_boiler_state"
42
-
43
- # Climate constants
44
- ACTIVE_PRESET: Final = "active_preset"
45
- CLIMATE_MODE: Final = "climate_mode"
46
- CONTROL_STATE: Final = "control_state"
47
- COOLING_PRESENT: Final ="cooling_present"
48
- DEV_CLASS: Final = "dev_class"
49
- NONE : Final = "None"
50
- TARGET_TEMP: Final = "setpoint"
51
- TARGET_TEMP_HIGH: Final = "setpoint_high"
52
- TARGET_TEMP_LOW: Final = "setpoint_low"
53
- THERMOSTAT: Final = "thermostat"
54
-
55
- # Config_flow constants
56
- ANNA_WITH_ADAM: Final = "anna_with_adam"
57
- CONTEXT: Final = "context"
58
- FLOW_ID: Final = "flow_id"
59
- FLOW_NET: Final = "Network: Smile/Stretch"
60
- FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
61
- FLOW_STRETCH: Final = "Stretch (Stretch)"
62
- FLOW_TYPE: Final = "flow_type"
63
- INIT: Final = "init"
64
- PRODUCT: Final = "product"
65
  SMILE_OPEN_THERM: Final = "smile_open_therm"
66
  SMILE_THERMO: Final = "smile_thermo"
67
- TITLE_PLACEHOLDERS: Final = "title_placeholders"
68
- VERSION: Final = "version"
69
-
70
- # Entity constants
71
- AVAILABLE: Final = "available"
72
- FIRMWARE: Final = "firmware"
73
- GATEWAY_ID: Final = "gateway_id"
74
- HARDWARE: Final = "hardware"
75
- MODEL: Final = "model"
76
- MODEL_ID: Final = "model_id"
77
- SMILE_NAME: Final = "smile_name"
78
- VENDOR: Final = "vendor"
79
-
80
- # Number constants
81
- MAX_BOILER_TEMP: Final = "maximum_boiler_temperature"
82
- MAX_DHW_TEMP: Final = "max_dhw_temperature"
83
- LOWER_BOUND: Final = "lower_bound"
84
- RESOLUTION: Final = "resolution"
85
- TEMPERATURE_OFFSET: Final = "temperature_offset"
86
- UPPER_BOUND: Final = "upper_bound"
87
-
88
- # Sensor constants
89
- DHW_TEMP: Final = "dhw_temperature"
90
- DHW_SETPOINT: Final = "domestic_hot_water_setpoint"
91
- EL_CONSUMED: Final = "electricity_consumed"
92
- EL_CONS_INTERVAL: Final = "electricity_consumed_interval"
93
- EL_CONS_OP_CUMULATIVE: Final = "electricity_consumed_off_peak_cumulative"
94
- EL_CONS_OP_INTERVAL: Final = "electricity_consumed_off_peak_interval"
95
- EL_CONS_OP_POINT: Final = "electricity_consumed_off_peak_point"
96
- EL_CONS_P_CUMULATIVE: Final = "electricity_consumed_peak_cumulative"
97
- EL_CONS_P_INTERVAL: Final = "electricity_consumed_peak_interval"
98
- EL_CONS_P_POINT: Final = "electricity_consumed_peak_point"
99
- EL_CONS_POINT: Final = "electricity_consumed_point"
100
- EL_PH1_CONSUMED: Final = "electricity_phase_one_consumed"
101
- EL_PH2_CONSUMED: Final = "electricity_phase_two_consumed"
102
- EL_PH3_CONSUMED: Final = "electricity_phase_three_consumed"
103
- EL_PH1_PRODUCED: Final = "electricity_phase_one_produced"
104
- EL_PH2_PRODUCED: Final = "electricity_phase_two_produced"
105
- EL_PH3_PRODUCED: Final = "electricity_phase_three_produced"
106
- EL_PRODUCED: Final = "electricity_produced"
107
- EL_PROD_INTERVAL: Final = "electricity_produced_interval"
108
- EL_PROD_OP_CUMULATIVE: Final = "electricity_produced_off_peak_cumulative"
109
- EL_PROD_OP_INTERVAL: Final = "electricity_produced_off_peak_interval"
110
- EL_PROD_OP_POINT: Final = "electricity_produced_off_peak_point"
111
- EL_PROD_P_CUMULATIVE: Final = "electricity_produced_peak_cumulative"
112
- EL_PROD_P_INTERVAL: Final = "electricity_produced_peak_interval"
113
- EL_PROD_P_POINT: Final = "electricity_produced_peak_point"
114
- EL_PROD_POINT: Final = "electricity_produced_point"
115
- GAS_CONS_CUMULATIVE: Final = "gas_consumed_cumulative"
116
- GAS_CONS_INTERVAL: Final = "gas_consumed_interval"
117
- INTENDED_BOILER_TEMP: Final = "intended_boiler_temperature"
118
- MOD_LEVEL: Final = "modulation_level"
119
- NET_EL_POINT: Final = "net_electricity_point"
120
- NET_EL_CUMULATIVE: Final = "net_electricity_cumulative"
121
- OUTDOOR_AIR_TEMP: Final = "outdoor_air_temperature"
122
- OUTDOOR_TEMP: Final = "outdoor_temperature"
123
- RETURN_TEMP: Final = "return_temperature"
124
- SENSORS: Final = "sensors"
125
- SOLAR_IRRADIANCE: Final = "solar_irradiance"
126
- TEMP_DIFF: Final = "temperature_difference"
127
- VALVE_POS: Final = "valve_position"
128
- VOLTAGE_PH1: Final = "voltage_phase_one"
129
- VOLTAGE_PH2: Final = "voltage_phase_two"
130
- VOLTAGE_PH3: Final = "voltage_phase_three"
131
- WATER_TEMP: Final = "water_temperature"
132
- WATER_PRESSURE: Final = "water_pressure"
133
-
134
- # Select constants
135
- AVAILABLE_SCHEDULES: Final = "available_schedules"
136
- DHW_MODE: Final = "dhw_mode"
137
- DHW_MODES: Final = "dhw_modes"
138
- GATEWAY_MODE: Final = "gateway_mode"
139
- GATEWAY_MODES: Final = "gateway_modes"
140
- REGULATION_MODE: Final = "regulation_mode"
141
- REGULATION_MODES: Final = "regulation_modes"
142
- SELECT_DHW_MODE: Final = "select_dhw_mode"
143
- SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
144
- SELECT_REGULATION_MODE: Final = "select_regulation_mode"
145
- SELECT_SCHEDULE: Final = "select_schedule"
146
-
147
- # Switch constants
148
- DHW_CM_SWITCH: Final = "dhw_cm_switch"
149
- LOCK: Final = "lock"
150
- MEMBERS: Final ="members"
151
- RELAY: Final = "relay"
152
- COOLING_ENA_SWITCH: Final ="cooling_ena_switch"
153
- SWITCHES: Final = "switches"
154
-
155
- # Weather constants
156
- CONDITION: Final ="weather_description"
157
- HUMIDITY: Final = "humidity"
158
- TEMPERATURE: Final ="outdoor_temperature"
159
- WEATHER: Final = "weather"
160
- WINDSPEED: Final ="wind_speed"
161
- WIND_BEARING: Final ="wind_bearing"
162
-
163
- # Default directives
164
- DEFAULT_PORT: Final[int] = 80
165
- DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
166
- "power": timedelta(seconds=10),
167
- "stretch": timedelta(seconds=60),
168
- "thermostat": timedelta(seconds=60),
169
- }
170
- DEFAULT_TIMEOUT: Final[int] = 30
171
- DEFAULT_USERNAME: Final = "smile"
172
 
173
- # --- Const for Plugwise Smile and Stretch
174
  PLATFORMS: Final[list[str]] = [
175
  Platform.BINARY_SENSOR,
176
  Platform.BUTTON,
@@ -179,20 +36,7 @@
179
  Platform.SELECT,
180
  Platform.SENSOR,
181
  Platform.SWITCH,
182
- Platform.WEATHER,
183
  ]
184
- SERVICE_DELETE: Final = "delete_notification"
185
- SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
186
-
187
- # Climate const:
188
- MASTER_THERMOSTATS: Final[list[str]] = [
189
- "thermostat",
190
- "zone_thermometer",
191
- "zone_thermostat",
192
- "thermostatic_radiator_valve",
193
- ]
194
-
195
- # Config_flow const:
196
  ZEROCONF_MAP: Final[dict[str, str]] = {
197
  "smile": "Smile P1",
198
  "smile_thermo": "Smile Anna",
@@ -211,10 +55,34 @@
211
  "select_gateway_mode",
212
  "select_regulation_mode",
213
  "select_schedule",
 
214
  ]
215
  type SelectOptionsType = Literal[
 
216
  "dhw_modes",
217
  "gateway_modes",
218
  "regulation_modes",
219
- "available_schedules",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  ]
 
 
 
 
 
 
 
 
6
 
7
  from homeassistant.const import Platform
8
 
 
 
9
  DOMAIN: Final = "plugwise"
10
 
11
  LOGGER = logging.getLogger(__package__)
12
 
13
+ ANNA_WITH_ADAM: Final = "anna_with_adam"
14
  API: Final = "api"
15
+ AVAILABLE: Final = "available"
16
+ DEV_CLASS: Final = "dev_class"
17
+ FLOW_SMILE: Final = "smile (Adam/Anna/P1)"
18
+ FLOW_STRETCH: Final = "stretch (Stretch)"
19
+ FLOW_TYPE: Final = "flow_type"
20
  GATEWAY: Final = "gateway"
21
  LOCATION: Final = "location"
22
+ PW_TYPE: Final = "plugwise_type"
23
  REBOOT: Final = "reboot"
24
  SMILE: Final = "smile"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  SMILE_OPEN_THERM: Final = "smile_open_therm"
26
  SMILE_THERMO: Final = "smile_thermo"
27
+ STRETCH: Final = "stretch"
28
+ STRETCH_USERNAME: Final = "stretch"
29
+ UNKNOWN_SMILE: Final = "Unknown Smile"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
 
31
  PLATFORMS: Final[list[str]] = [
32
  Platform.BINARY_SENSOR,
33
  Platform.BUTTON,
 
36
  Platform.SELECT,
37
  Platform.SENSOR,
38
  Platform.SWITCH,
 
39
  ]
 
 
 
 
 
 
 
 
 
 
 
 
40
  ZEROCONF_MAP: Final[dict[str, str]] = {
41
  "smile": "Smile P1",
42
  "smile_thermo": "Smile Anna",
 
55
  "select_gateway_mode",
56
  "select_regulation_mode",
57
  "select_schedule",
58
+ "select_zone_profile",
59
  ]
60
  type SelectOptionsType = Literal[
61
+ "available_schedules",
62
  "dhw_modes",
63
  "gateway_modes",
64
  "regulation_modes",
65
+ "zone_profiles",
66
+ ]
67
+
68
+ # Default directives
69
+ DEFAULT_MAX_TEMP: Final = 30
70
+ DEFAULT_MIN_TEMP: Final = 4
71
+ DEFAULT_PORT: Final = 80
72
+ DEFAULT_UPDATE_INTERVAL = timedelta(seconds=60)
73
+ DEFAULT_USERNAME: Final = "smile"
74
+ P1_UPDATE_INTERVAL = timedelta(seconds=10)
75
+
76
+ MASTER_THERMOSTATS: Final[list[str]] = [
77
+ "thermostat",
78
+ "thermostatic_radiator_valve",
79
+ "zone_thermometer",
80
+ "zone_thermostat",
81
  ]
82
+
83
+ # Select constants
84
+ SELECT_DHW_MODE: Final = "select_dhw_mode"
85
+ SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
86
+ SELECT_REGULATION_MODE: Final = "select_regulation_mode"
87
+ SELECT_SCHEDULE: Final = "select_schedule"
88
+ SELECT_ZONE_PROFILE: Final = "select_zone_profile"
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py RENAMED
@@ -1,11 +1,13 @@
1
  """DataUpdateCoordinator for Plugwise."""
2
 
3
- from datetime import timedelta
4
 
5
- from plugwise import PlugwiseData, Smile
 
6
  from plugwise.exceptions import (
7
  ConnectionFailedError,
8
  InvalidAuthentication,
 
9
  InvalidXMLError,
10
  PlugwiseError,
11
  ResponseError,
@@ -13,143 +15,201 @@
13
  )
14
 
15
  from homeassistant.config_entries import ConfigEntry
16
- from homeassistant.const import (
17
- CONF_HOST,
18
- CONF_PASSWORD,
19
- CONF_PORT,
20
- CONF_SCAN_INTERVAL, # pw-beta options
21
- CONF_USERNAME,
22
- )
23
  from homeassistant.core import HomeAssistant
24
  from homeassistant.exceptions import ConfigEntryError
25
  from homeassistant.helpers import device_registry as dr
26
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
27
  from homeassistant.helpers.debounce import Debouncer
28
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
29
- from packaging.version import Version
30
 
31
- from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, GATEWAY_ID, LOGGER
 
 
 
 
 
 
 
32
 
 
33
 
34
- class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
35
- """Class to manage fetching Plugwise data from single endpoint."""
36
 
37
- _connected: bool = False
 
38
 
39
- config_entry: ConfigEntry
40
 
41
- def __init__(
42
- self,
43
- hass: HomeAssistant,
44
- cooldown: float,
45
- update_interval: timedelta = timedelta(seconds=60),
46
- ) -> None: # pw-beta cooldown
47
  """Initialize the coordinator."""
48
  super().__init__(
49
  hass,
50
  LOGGER,
 
51
  name=DOMAIN,
52
- # Core directly updates from const's DEFAULT_SCAN_INTERVAL
53
- # Upstream check correct progress for adjusting
54
- update_interval=update_interval,
55
  # Don't refresh immediately, give the device time to process
56
  # the change in state before we query it.
57
  request_refresh_debouncer=Debouncer(
58
  hass,
59
  LOGGER,
60
- cooldown=cooldown,
61
  immediate=False,
62
  ),
63
  )
64
 
65
  self.api = Smile(
66
  host=self.config_entry.data[CONF_HOST],
 
67
  password=self.config_entry.data[CONF_PASSWORD],
68
- port=self.config_entry.data[CONF_PORT],
69
- username=self.config_entry.data[CONF_USERNAME],
70
  websession=async_get_clientsession(hass, verify_ssl=False),
71
  )
 
72
  self._current_devices: set[str] = set()
 
 
73
  self.new_devices: set[str] = set()
74
- self.update_interval = update_interval
75
 
76
  async def _connect(self) -> None:
77
- """Connect to the Plugwise Smile."""
 
 
 
78
  version = await self.api.connect()
79
  self._connected = isinstance(version, Version)
80
- if self._connected:
81
- self.api.get_all_gateway_entities()
82
- self.update_interval = DEFAULT_SCAN_INTERVAL.get(
83
- self.api.smile_type, timedelta(seconds=60)
84
- ) # pw-beta options scan-interval
85
- if (custom_time := self.config_entry.options.get(CONF_SCAN_INTERVAL)) is not None:
86
- self.update_interval = timedelta(
87
- seconds=int(custom_time)
88
- ) # pragma: no cover # pw-beta options
89
 
90
- LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- async def _async_update_data(self) -> PlugwiseData:
 
93
  """Fetch data from Plugwise."""
94
- data = PlugwiseData(devices={}, gateway={})
95
  try:
96
  if not self._connected:
97
  await self._connect()
98
  data = await self.api.async_update()
99
  except ConnectionFailedError as err:
100
- raise UpdateFailed("Failed to connect") from err
 
 
 
101
  except InvalidAuthentication as err:
102
- raise ConfigEntryError("Authentication failed") from err
 
 
 
 
 
 
 
 
103
  except (InvalidXMLError, ResponseError) as err:
104
  raise UpdateFailed(
105
- f"Invalid XML data or error from Plugwise device: {err}"
 
106
  ) from err
107
  except PlugwiseError as err:
108
- raise UpdateFailed("Data incomplete or missing") from err
 
 
 
109
  except UnsupportedDeviceError as err:
110
- raise ConfigEntryError("Device with unsupported firmware") from err
111
- else:
112
- LOGGER.debug(f"{self.api.smile_name} data: %s", data)
113
- await self.async_add_remove_devices(data, self.config_entry)
114
 
 
 
115
  return data
116
 
117
- async def async_add_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
118
  """Add new Plugwise devices, remove non-existing devices."""
119
- # Check for new or removed devices
120
- self.new_devices = set(data.devices) - self._current_devices
121
- removed_devices = self._current_devices - set(data.devices)
122
- self._current_devices = set(data.devices)
 
 
 
 
 
123
 
124
- if removed_devices:
125
- await self.async_remove_devices(data, entry)
 
 
 
 
126
 
127
- async def async_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
128
  """Clean registries when removed devices found."""
129
  device_reg = dr.async_get(self.hass)
130
- device_list = dr.async_entries_for_config_entry(
131
- device_reg, self.config_entry.entry_id
132
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- # First find the Plugwise via_device
135
- gateway_device = device_reg.async_get_device({(DOMAIN, data.gateway[GATEWAY_ID])})
136
- if gateway_device is not None:
137
- via_device_id = gateway_device.id
138
-
139
- # Then remove the connected orphaned device(s)
140
- for device_entry in device_list:
141
- for identifier in device_entry.identifiers:
142
- if identifier[0] == DOMAIN:
143
- if (
144
- device_entry.via_device_id == via_device_id
145
- and identifier[1] not in data.devices
146
- ):
147
- device_reg.async_update_device(
148
- device_entry.id, remove_config_entry_id=entry.entry_id
149
- )
150
- LOGGER.debug(
151
- "Removed %s device/zone %s %s from device_registry",
152
- DOMAIN,
153
- device_entry.model,
154
- identifier[1],
155
- )
 
1
  """DataUpdateCoordinator for Plugwise."""
2
 
3
+ from typing import override
4
 
5
+ from packaging.version import Version
6
+ from plugwise import GwEntityData, Smile
7
  from plugwise.exceptions import (
8
  ConnectionFailedError,
9
  InvalidAuthentication,
10
+ InvalidSetupError,
11
  InvalidXMLError,
12
  PlugwiseError,
13
  ResponseError,
 
15
  )
16
 
17
  from homeassistant.config_entries import ConfigEntry
18
+ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
 
 
 
 
 
 
19
  from homeassistant.core import HomeAssistant
20
  from homeassistant.exceptions import ConfigEntryError
21
  from homeassistant.helpers import device_registry as dr
22
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
23
  from homeassistant.helpers.debounce import Debouncer
24
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
25
 
26
+ from .const import (
27
+ DEFAULT_PORT,
28
+ DEFAULT_UPDATE_INTERVAL,
29
+ DEFAULT_USERNAME,
30
+ DOMAIN,
31
+ LOGGER,
32
+ P1_UPDATE_INTERVAL,
33
+ )
34
 
35
+ type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
36
 
 
 
37
 
38
+ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData]]):
39
+ """Class to manage fetching Plugwise data from single endpoint."""
40
 
41
+ config_entry: PlugwiseConfigEntry
42
 
43
+ def __init__(self, hass: HomeAssistant, config_entry: PlugwiseConfigEntry) -> None:
 
 
 
 
 
44
  """Initialize the coordinator."""
45
  super().__init__(
46
  hass,
47
  LOGGER,
48
+ config_entry=config_entry,
49
  name=DOMAIN,
50
+ update_interval=DEFAULT_UPDATE_INTERVAL,
 
 
51
  # Don't refresh immediately, give the device time to process
52
  # the change in state before we query it.
53
  request_refresh_debouncer=Debouncer(
54
  hass,
55
  LOGGER,
56
+ cooldown=1.5,
57
  immediate=False,
58
  ),
59
  )
60
 
61
  self.api = Smile(
62
  host=self.config_entry.data[CONF_HOST],
63
+ username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
64
  password=self.config_entry.data[CONF_PASSWORD],
65
+ port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
 
66
  websession=async_get_clientsession(hass, verify_ssl=False),
67
  )
68
+ self._connected: bool = False
69
  self._current_devices: set[str] = set()
70
+ self._firmware_list: dict[str, str | None] = {}
71
+ self._stored_devices: set[str] = set()
72
  self.new_devices: set[str] = set()
 
73
 
74
  async def _connect(self) -> None:
75
+ """Connect to the Plugwise Smile.
76
+
77
+ A Version object is received when the connection succeeds.
78
+ """
79
  version = await self.api.connect()
80
  self._connected = isinstance(version, Version)
81
+ if self._connected and self.api.smile.type == "power":
82
+ self.update_interval = P1_UPDATE_INTERVAL
 
 
 
 
 
 
 
83
 
84
+ @override
85
+ async def _async_setup(self) -> None:
86
+ """Initialize the update_data process."""
87
+ device_reg = dr.async_get(self.hass)
88
+ device_entries = dr.async_entries_for_config_entry(
89
+ device_reg, self.config_entry.entry_id
90
+ )
91
+ self._stored_devices = {
92
+ identifier[1]
93
+ for device_entry in device_entries
94
+ for identifier in device_entry.identifiers
95
+ if identifier[0] == DOMAIN
96
+ }
97
 
98
+ @override
99
+ async def _async_update_data(self) -> dict[str, GwEntityData]:
100
  """Fetch data from Plugwise."""
 
101
  try:
102
  if not self._connected:
103
  await self._connect()
104
  data = await self.api.async_update()
105
  except ConnectionFailedError as err:
106
+ raise UpdateFailed(
107
+ translation_domain=DOMAIN,
108
+ translation_key="failed_to_connect",
109
+ ) from err
110
  except InvalidAuthentication as err:
111
+ raise ConfigEntryError(
112
+ translation_domain=DOMAIN,
113
+ translation_key="authentication_failed",
114
+ ) from err
115
+ except InvalidSetupError as err:
116
+ raise ConfigEntryError(
117
+ translation_domain=DOMAIN,
118
+ translation_key="invalid_setup",
119
+ ) from err
120
  except (InvalidXMLError, ResponseError) as err:
121
  raise UpdateFailed(
122
+ translation_domain=DOMAIN,
123
+ translation_key="response_error",
124
  ) from err
125
  except PlugwiseError as err:
126
+ raise UpdateFailed(
127
+ translation_domain=DOMAIN,
128
+ translation_key="data_incomplete_or_missing",
129
+ ) from err
130
  except UnsupportedDeviceError as err:
131
+ raise ConfigEntryError(
132
+ translation_domain=DOMAIN,
133
+ translation_key="unsupported_firmware",
134
+ ) from err
135
 
136
+ self._add_remove_devices(data)
137
+ self._update_device_firmware(data)
138
  return data
139
 
140
+ def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
141
  """Add new Plugwise devices, remove non-existing devices."""
142
+ set_of_data = set(data)
143
+ # Check for new or removed devices,
144
+ # 'new_devices' contains all devices present in 'data'
145
+ # at init ('self._current_devices' is empty) this is
146
+ # required for the proper initialization of all the
147
+ # present platform entities.
148
+ self.new_devices = set_of_data - self._current_devices
149
+ for device_id in self.new_devices:
150
+ self._firmware_list.setdefault(device_id, data[device_id].get("firmware"))
151
 
152
+ current_devices = (
153
+ self._stored_devices if not self._current_devices else self._current_devices
154
+ )
155
+ self._current_devices = set_of_data
156
+ if removed_devices := (current_devices - set_of_data): # device(s) to remove
157
+ self._remove_devices(removed_devices)
158
 
159
+ def _remove_devices(self, removed_devices: set[str]) -> None:
160
  """Clean registries when removed devices found."""
161
  device_reg = dr.async_get(self.hass)
162
+ for device_id in removed_devices:
163
+ if (
164
+ device_entry := device_reg.async_get_device_by_identifier(
165
+ (DOMAIN, device_id), self.config_entry.entry_id
166
+ )
167
+ ) is not None:
168
+ device_reg.async_update_device(
169
+ device_entry.id, remove_config_entry_id=self.config_entry.entry_id
170
+ )
171
+ LOGGER.debug(
172
+ "%s %s %s removed from device_registry",
173
+ DOMAIN,
174
+ device_entry.model,
175
+ device_id,
176
+ )
177
+
178
+ self._firmware_list.pop(device_id, None)
179
+
180
+ def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
181
+ """Detect firmware changes and update the device registry."""
182
+ for device_id, device in data.items():
183
+ # Only update firmware when the key is present and not None, to avoid
184
+ # wiping stored firmware on partial or transient updates.
185
+ if "firmware" not in device:
186
+ continue
187
+ new_firmware = device.get("firmware")
188
+ if new_firmware is None:
189
+ continue
190
+ if (
191
+ device_id in self._firmware_list
192
+ and new_firmware != self._firmware_list[device_id]
193
+ ):
194
+ updated = self._update_firmware_in_dr(device_id, new_firmware)
195
+ if updated:
196
+ self._firmware_list[device_id] = new_firmware
197
+
198
+ def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> bool:
199
+ """Update device sw_version in device_registry."""
200
+ device_reg = dr.async_get(self.hass)
201
+ if (
202
+ device_entry := device_reg.async_get_device_by_identifier(
203
+ (DOMAIN, device_id), self.config_entry.entry_id
204
+ )
205
+ ) is not None:
206
+ device_reg.async_update_device(device_entry.id, sw_version=firmware)
207
+ LOGGER.debug(
208
+ "Firmware in device_registry updated for %s %s %s",
209
+ DOMAIN,
210
+ device_entry.model,
211
+ device_id,
212
+ )
213
+ return True
214
 
215
+ return False # pragma: no cover
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/diagnostics.py RENAMED
@@ -1,12 +1,10 @@
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(
@@ -14,7 +12,4 @@
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
- }
 
1
  """Diagnostics support for Plugwise."""
2
 
 
 
3
  from typing import Any
4
 
5
  from homeassistant.core import HomeAssistant
6
 
7
+ from .coordinator import PlugwiseConfigEntry
8
 
9
 
10
  async def async_get_config_entry_diagnostics(
 
12
  ) -> dict[str, Any]:
13
  """Return diagnostics for a config entry."""
14
  coordinator = entry.runtime_data
15
+ return coordinator.data
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py RENAMED
@@ -1,8 +1,8 @@
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 (
@@ -12,21 +12,7 @@
12
  )
13
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
14
 
15
- from .const import (
16
- AVAILABLE,
17
- DOMAIN,
18
- FIRMWARE,
19
- GATEWAY_ID,
20
- HARDWARE,
21
- MAC_ADDRESS,
22
- MODEL,
23
- MODEL_ID,
24
- SMILE_NAME,
25
- VENDOR,
26
- ZIGBEE_MAC_ADDRESS,
27
- )
28
-
29
- # Upstream consts
30
  from .coordinator import PlugwiseDataUpdateCoordinator
31
 
32
 
@@ -44,47 +30,52 @@
44
  super().__init__(coordinator)
45
  self._dev_id = device_id
46
 
47
- configuration_url: str | None = None
48
- if entry := self.coordinator.config_entry:
49
- configuration_url = f"http://{entry.data[CONF_HOST]}"
 
 
 
 
 
 
 
50
 
51
- data = coordinator.data.devices[device_id]
52
  connections = set()
53
- if mac := data.get(MAC_ADDRESS):
54
  connections.add((CONNECTION_NETWORK_MAC, mac))
55
- if mac := data.get(ZIGBEE_MAC_ADDRESS):
56
- connections.add((CONNECTION_ZIGBEE, mac))
57
 
 
58
  self._attr_device_info = DeviceInfo(
59
  configuration_url=configuration_url,
60
  identifiers={(DOMAIN, device_id)},
61
  connections=connections,
62
- manufacturer=data.get(VENDOR),
63
- model=data.get(MODEL),
64
- model_id=data.get(MODEL_ID),
65
- name=coordinator.data.gateway[SMILE_NAME],
66
- sw_version=data.get(FIRMWARE),
67
- hw_version=data.get(HARDWARE),
68
  )
69
 
70
- if device_id != coordinator.data.gateway[GATEWAY_ID]:
 
71
  self._attr_device_info.update(
72
  {
73
- ATTR_NAME: data.get(ATTR_NAME),
74
- ATTR_VIA_DEVICE: (
75
- DOMAIN,
76
- str(self.coordinator.data.gateway[GATEWAY_ID]),
77
- ),
78
  }
79
  )
80
 
81
  @property
 
82
  def available(self) -> bool:
83
  """Return if entity is available."""
84
  return (
85
- # Upstream: Do not change the AVAILABLE line below: some Plugwise devices and zones
86
- # Upstream: do not provide their availability-status!
87
- self._dev_id in self.coordinator.data.devices
88
  and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
89
  and super().available
90
  )
@@ -92,9 +83,4 @@
92
  @property
93
  def device(self) -> GwEntityData:
94
  """Return data for this device."""
95
- return self.coordinator.data.devices[self._dev_id]
96
-
97
- async def async_added_to_hass(self) -> None:
98
- """Subscribe to updates."""
99
- self._handle_coordinator_update()
100
- await super().async_added_to_hass()
 
1
  """Generic Plugwise Entity Class."""
2
 
3
+ from typing import override
4
 
5
+ from plugwise import GwEntityData
6
 
7
  from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
8
  from homeassistant.helpers.device_registry import (
 
12
  )
13
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
14
 
15
+ from .const import AVAILABLE, DOMAIN
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  from .coordinator import PlugwiseDataUpdateCoordinator
17
 
18
 
 
30
  super().__init__(coordinator)
31
  self._dev_id = device_id
32
 
33
+ api = coordinator.api
34
+ gateway_id = api.gateway_id
35
+ entry = coordinator.config_entry
36
+
37
+ # Link configuration-URL for the gateway device
38
+ configuration_url = (
39
+ f"http://{entry.data[CONF_HOST]}"
40
+ if device_id == gateway_id and entry
41
+ else None
42
+ )
43
 
44
+ # Build connections set
45
  connections = set()
46
+ if mac := self.device.get("mac_address"):
47
  connections.add((CONNECTION_NETWORK_MAC, mac))
48
+ if zigbee_mac := self.device.get("zigbee_mac_address"):
49
+ connections.add((CONNECTION_ZIGBEE, zigbee_mac))
50
 
51
+ # Set base device info
52
  self._attr_device_info = DeviceInfo(
53
  configuration_url=configuration_url,
54
  identifiers={(DOMAIN, device_id)},
55
  connections=connections,
56
+ manufacturer=self.device.get("vendor"),
57
+ model=self.device.get("model"),
58
+ model_id=self.device.get("model_id"),
59
+ name=api.smile.name,
60
+ sw_version=self.device.get("firmware"),
61
+ hw_version=self.device.get("hardware"),
62
  )
63
 
64
+ # Add extra info if not the gateway device
65
+ if device_id != gateway_id:
66
  self._attr_device_info.update(
67
  {
68
+ ATTR_NAME: self.device.get(ATTR_NAME),
69
+ ATTR_VIA_DEVICE: (DOMAIN, gateway_id),
 
 
 
70
  }
71
  )
72
 
73
  @property
74
+ @override
75
  def available(self) -> bool:
76
  """Return if entity is available."""
77
  return (
78
+ self._dev_id in self.coordinator.data
 
 
79
  and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
80
  and super().available
81
  )
 
83
  @property
84
  def device(self) -> GwEntityData:
85
  """Return data for this device."""
86
+ return self.coordinator.data[self._dev_id]
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/icons.json RENAMED
@@ -66,11 +66,11 @@
66
  "dhw_mode": {
67
  "default": "mdi:shower",
68
  "state": {
 
 
69
  "comfort": "mdi:sofa",
70
  "eco": "mdi:leaf",
71
- "off": "mdi:circle-off-outline",
72
- "boost": "mdi:rocket-launch",
73
- "auto": "mdi:auto-mode"
74
  }
75
  },
76
  "gateway_mode": {
@@ -84,11 +84,11 @@
84
  "regulation_mode": {
85
  "default": "mdi:hvac",
86
  "state": {
87
- "bleeding_hot": "mdi:fire-circle",
88
  "bleeding_cold": "mdi:water-circle",
89
- "off": "mdi:circle-off-outline",
 
90
  "heating": "mdi:radiator",
91
- "cooling": "mdi:snowflake"
92
  }
93
  },
94
  "select_schedule": {
@@ -120,8 +120,5 @@
120
  "default": "mdi:lock"
121
  }
122
  }
123
- },
124
- "services": {
125
- "delete_notification": "mdi:trash-can"
126
  }
127
  }
 
66
  "dhw_mode": {
67
  "default": "mdi:shower",
68
  "state": {
69
+ "auto": "mdi:auto-mode",
70
+ "boost": "mdi:rocket-launch",
71
  "comfort": "mdi:sofa",
72
  "eco": "mdi:leaf",
73
+ "off": "mdi:circle-off-outline"
 
 
74
  }
75
  },
76
  "gateway_mode": {
 
84
  "regulation_mode": {
85
  "default": "mdi:hvac",
86
  "state": {
 
87
  "bleeding_cold": "mdi:water-circle",
88
+ "bleeding_hot": "mdi:fire-circle",
89
+ "cooling": "mdi:snowflake",
90
  "heating": "mdi:radiator",
91
+ "off": "mdi:circle-off-outline"
92
  }
93
  },
94
  "select_schedule": {
 
120
  "default": "mdi:lock"
121
  }
122
  }
 
 
 
123
  }
124
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json RENAMED
@@ -1,13 +1,13 @@
1
  {
2
  "domain": "plugwise",
3
- "name": "Plugwise Beta",
4
  "codeowners": ["@CoMPaTech", "@bouwew"],
5
  "config_flow": true,
6
- "documentation": "https://github.com/plugwise/plugwise-beta",
7
  "integration_type": "hub",
8
  "iot_class": "local_polling",
9
  "loggers": ["plugwise"],
10
- "requirements": ["https://test-files.pythonhosted.org/packages/ae/29/ec86637a394e58a70dc296a33d0c18557238b82e668aac19f8195101bacd/plugwise-1.7.0a1.tar.gz#plugwise==1.7.0a1"],
11
- "version": "0.56.0a0",
12
  "zeroconf": ["_plugwise._tcp.local."]
13
  }
 
1
  {
2
  "domain": "plugwise",
3
+ "name": "Plugwise",
4
  "codeowners": ["@CoMPaTech", "@bouwew"],
5
  "config_flow": true,
6
+ "documentation": "https://www.home-assistant.io/integrations/plugwise",
7
  "integration_type": "hub",
8
  "iot_class": "local_polling",
9
  "loggers": ["plugwise"],
10
+ "quality_scale": "platinum",
11
+ "requirements": ["plugwise==1.11.4"],
12
  "zeroconf": ["_plugwise._tcp.local."]
13
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py RENAMED
@@ -1,8 +1,7 @@
1
  """Number platform for Plugwise integration."""
2
 
3
- from __future__ import annotations
4
-
5
  from dataclasses import dataclass
 
6
 
7
  from homeassistant.components.number import (
8
  NumberDeviceClass,
@@ -12,26 +11,14 @@
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 (
19
- LOGGER,
20
- LOWER_BOUND,
21
- MAX_BOILER_TEMP,
22
- MAX_DHW_TEMP,
23
- RESOLUTION,
24
- TEMPERATURE_OFFSET,
25
- UPPER_BOUND,
26
- NumberType,
27
- )
28
 
29
- # Upstream consts
30
- from .coordinator import PlugwiseDataUpdateCoordinator
31
  from .entity import PlugwiseEntity
32
  from .util import plugwise_command
33
 
34
- PARALLEL_UPDATES = 0 # Upstream
35
 
36
 
37
  @dataclass(frozen=True, kw_only=True)
@@ -41,25 +28,24 @@
41
  key: NumberType
42
 
43
 
44
- # Upstream + is there a reason we didn't rename this one prefixed?
45
  NUMBER_TYPES = (
46
  PlugwiseNumberEntityDescription(
47
- key=MAX_BOILER_TEMP,
48
- translation_key=MAX_BOILER_TEMP,
49
  device_class=NumberDeviceClass.TEMPERATURE,
50
  entity_category=EntityCategory.CONFIG,
51
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
52
  ),
53
  PlugwiseNumberEntityDescription(
54
- key=MAX_DHW_TEMP,
55
- translation_key=MAX_DHW_TEMP,
56
  device_class=NumberDeviceClass.TEMPERATURE,
57
  entity_category=EntityCategory.CONFIG,
58
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
59
  ),
60
  PlugwiseNumberEntityDescription(
61
- key=TEMPERATURE_OFFSET,
62
- translation_key=TEMPERATURE_OFFSET,
63
  device_class=NumberDeviceClass.TEMPERATURE,
64
  entity_category=EntityCategory.CONFIG,
65
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -70,10 +56,9 @@
70
  async def async_setup_entry(
71
  hass: HomeAssistant,
72
  entry: PlugwiseConfigEntry,
73
- async_add_entities: AddEntitiesCallback,
74
  ) -> None:
75
- """Set up Plugwise number platform from a config entry."""
76
- # Upstream above to adhere to standard used
77
  coordinator = entry.runtime_data
78
 
79
  @callback
@@ -82,28 +67,12 @@
82
  if not coordinator.new_devices:
83
  return
84
 
85
- # Upstream consts
86
- # async_add_entities(
87
- # PlugwiseNumberEntity(coordinator, device_id, description)
88
- # for device_id in coordinator.new_devices
89
- # for description in NUMBER_TYPES
90
- # if description.key in coordinator.data.devices[device_id]
91
- # )
92
-
93
- # pw-beta alternative for debugging
94
- entities: list[PlugwiseNumberEntity] = []
95
- for device_id in coordinator.new_devices:
96
- device = coordinator.data.devices[device_id]
97
- for description in NUMBER_TYPES:
98
- if description.key in device:
99
- entities.append(
100
- PlugwiseNumberEntity(coordinator, device_id, description)
101
- )
102
- LOGGER.debug(
103
- "Add %s %s number", device["name"], description.translation_key
104
- )
105
-
106
- async_add_entities(entities)
107
 
108
  _add_entities()
109
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -122,28 +91,28 @@
122
  ) -> None:
123
  """Initiate Plugwise Number."""
124
  super().__init__(coordinator, device_id)
125
- self.actuator = self.device[description.key] # Upstream
 
 
 
126
  self.device_id = device_id
127
  self.entity_description = description
128
- self._attr_unique_id = f"{device_id}-{description.key}"
129
- self._attr_mode = NumberMode.BOX
130
- self._attr_native_max_value = self.device[description.key][UPPER_BOUND] # Upstream const
131
- self._attr_native_min_value = self.device[description.key][LOWER_BOUND] # Upstream const
132
 
133
- native_step = self.device[description.key][RESOLUTION] # Upstream const
134
- if description.key != TEMPERATURE_OFFSET: # Upstream const
135
  native_step = max(native_step, 0.5)
136
  self._attr_native_step = native_step
137
 
138
  @property
 
139
  def native_value(self) -> float:
140
  """Return the present setpoint value."""
141
  return self.device[self.entity_description.key]["setpoint"]
142
 
143
  @plugwise_command
 
144
  async def async_set_native_value(self, value: float) -> None:
145
  """Change to the new setpoint value."""
146
- await self.coordinator.api.set_number(self.device_id, self.entity_description.key, value)
147
- LOGGER.debug(
148
- "Setting %s to %s was successful", self.entity_description.key, value
149
  )
 
1
  """Number platform for Plugwise integration."""
2
 
 
 
3
  from dataclasses import dataclass
4
+ from typing import override
5
 
6
  from homeassistant.components.number import (
7
  NumberDeviceClass,
 
11
  )
12
  from homeassistant.const import EntityCategory, UnitOfTemperature
13
  from homeassistant.core import HomeAssistant, callback
14
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ from .const import NumberType
17
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
18
  from .entity import PlugwiseEntity
19
  from .util import plugwise_command
20
 
21
+ PARALLEL_UPDATES = 0
22
 
23
 
24
  @dataclass(frozen=True, kw_only=True)
 
28
  key: NumberType
29
 
30
 
 
31
  NUMBER_TYPES = (
32
  PlugwiseNumberEntityDescription(
33
+ key="maximum_boiler_temperature",
34
+ translation_key="maximum_boiler_temperature",
35
  device_class=NumberDeviceClass.TEMPERATURE,
36
  entity_category=EntityCategory.CONFIG,
37
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
38
  ),
39
  PlugwiseNumberEntityDescription(
40
+ key="max_dhw_temperature",
41
+ translation_key="max_dhw_temperature",
42
  device_class=NumberDeviceClass.TEMPERATURE,
43
  entity_category=EntityCategory.CONFIG,
44
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
45
  ),
46
  PlugwiseNumberEntityDescription(
47
+ key="temperature_offset",
48
+ translation_key="temperature_offset",
49
  device_class=NumberDeviceClass.TEMPERATURE,
50
  entity_category=EntityCategory.CONFIG,
51
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 
56
  async def async_setup_entry(
57
  hass: HomeAssistant,
58
  entry: PlugwiseConfigEntry,
59
+ async_add_entities: AddConfigEntryEntitiesCallback,
60
  ) -> None:
61
+ """Set up Plugwise number platform."""
 
62
  coordinator = entry.runtime_data
63
 
64
  @callback
 
67
  if not coordinator.new_devices:
68
  return
69
 
70
+ async_add_entities(
71
+ PlugwiseNumberEntity(coordinator, device_id, description)
72
+ for device_id in coordinator.new_devices
73
+ for description in NUMBER_TYPES
74
+ if description.key in coordinator.data[device_id]
75
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  _add_entities()
78
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
91
  ) -> None:
92
  """Initiate Plugwise Number."""
93
  super().__init__(coordinator, device_id)
94
+ self._attr_mode = NumberMode.BOX
95
+ self._attr_native_max_value = self.device[description.key]["upper_bound"]
96
+ self._attr_native_min_value = self.device[description.key]["lower_bound"]
97
+ self._attr_unique_id = f"{device_id}-{description.key}"
98
  self.device_id = device_id
99
  self.entity_description = description
 
 
 
 
100
 
101
+ native_step = self.device[description.key]["resolution"]
102
+ if description.key != "temperature_offset":
103
  native_step = max(native_step, 0.5)
104
  self._attr_native_step = native_step
105
 
106
  @property
107
+ @override
108
  def native_value(self) -> float:
109
  """Return the present setpoint value."""
110
  return self.device[self.entity_description.key]["setpoint"]
111
 
112
  @plugwise_command
113
+ @override
114
  async def async_set_native_value(self, value: float) -> None:
115
  """Change to the new setpoint value."""
116
+ await self.coordinator.api.set_number(
117
+ self.device_id, self.entity_description.key, value
 
118
  )
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py RENAMED
@@ -1,39 +1,28 @@
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 (
14
- AVAILABLE_SCHEDULES,
15
- DHW_MODE,
16
- DHW_MODES,
17
- GATEWAY_MODE,
18
- GATEWAY_MODES,
19
  LOCATION,
20
- LOGGER,
21
- REGULATION_MODE,
22
- REGULATION_MODES,
23
  SELECT_DHW_MODE,
24
  SELECT_GATEWAY_MODE,
25
  SELECT_REGULATION_MODE,
26
  SELECT_SCHEDULE,
 
27
  SelectOptionsType,
28
  SelectType,
29
  )
30
-
31
- # Upstream consts
32
- from .coordinator import PlugwiseDataUpdateCoordinator
33
  from .entity import PlugwiseEntity
34
  from .util import plugwise_command
35
 
36
- PARALLEL_UPDATES = 0 # Upstream
37
 
38
 
39
  @dataclass(frozen=True, kw_only=True)
@@ -44,30 +33,35 @@
44
  options_key: SelectOptionsType
45
 
46
 
47
- # Upstream + is there a reason we didn't rename this one prefixed?
48
  SELECT_TYPES = (
49
  PlugwiseSelectEntityDescription(
50
  key=SELECT_SCHEDULE,
51
  translation_key=SELECT_SCHEDULE,
52
- options_key=AVAILABLE_SCHEDULES,
53
  ),
54
  PlugwiseSelectEntityDescription(
55
  key=SELECT_REGULATION_MODE,
56
- translation_key=REGULATION_MODE,
57
  entity_category=EntityCategory.CONFIG,
58
- options_key=REGULATION_MODES,
59
  ),
60
  PlugwiseSelectEntityDescription(
61
  key=SELECT_DHW_MODE,
62
- translation_key=DHW_MODE,
63
  entity_category=EntityCategory.CONFIG,
64
- options_key=DHW_MODES,
65
  ),
66
  PlugwiseSelectEntityDescription(
67
  key=SELECT_GATEWAY_MODE,
68
- translation_key=GATEWAY_MODE,
 
 
 
 
 
 
69
  entity_category=EntityCategory.CONFIG,
70
- options_key=GATEWAY_MODES,
71
  ),
72
  )
73
 
@@ -75,9 +69,9 @@
75
  async def async_setup_entry(
76
  hass: HomeAssistant,
77
  entry: PlugwiseConfigEntry,
78
- async_add_entities: AddEntitiesCallback,
79
  ) -> None:
80
- """Set up Plugwise selector from a config entry."""
81
  coordinator = entry.runtime_data
82
 
83
  @callback
@@ -86,27 +80,12 @@
86
  if not coordinator.new_devices:
87
  return
88
 
89
- # Upstream consts
90
- # async_add_entities(
91
- # PlugwiseSelectEntity(coordinator, device_id, description)
92
- # for device_id in coordinator.new_devices
93
- # for description in SELECT_TYPES
94
- # if description.options_key in coordinator.data.devices[device_id]
95
- # )
96
- # pw-beta alternative for debugging
97
- entities: list[PlugwiseSelectEntity] = []
98
- for device_id in coordinator.new_devices:
99
- device = coordinator.data.devices[device_id]
100
- for description in SELECT_TYPES:
101
- if description.options_key in device:
102
- entities.append(
103
- PlugwiseSelectEntity(coordinator, device_id, description)
104
- )
105
- LOGGER.debug(
106
- "Add %s %s selector", device["name"], description.translation_key
107
- )
108
-
109
- async_add_entities(entities)
110
 
111
  _add_entities()
112
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -128,31 +107,34 @@
128
  self._attr_unique_id = f"{device_id}-{entity_description.key}"
129
  self.entity_description = entity_description
130
 
131
- self._location = device_id
132
- if (location := self.device.get(LOCATION)) is not None:
133
- self._location = location
 
 
 
134
 
135
  @property
136
- def current_option(self) -> str:
 
137
  """Return the selected entity option to represent the entity state."""
138
  return self.device[self.entity_description.key]
139
 
140
  @property
 
141
  def options(self) -> list[str]:
142
  """Return the available select-options."""
143
  return self.device[self.entity_description.options_key]
144
 
145
  @plugwise_command
 
146
  async def async_select_option(self, option: str) -> None:
147
  """Change to the selected entity option.
148
 
149
- Location ID and STATE_ON are required for the thermostat-schedule select.
 
 
150
  """
151
  await self.coordinator.api.set_select(
152
- self.entity_description.key, self._location, option, STATE_ON
153
- )
154
- LOGGER.debug(
155
- "Set %s to %s was successful",
156
- self.entity_description.key,
157
- option,
158
  )
 
1
  """Plugwise Select component for Home Assistant."""
2
 
 
 
3
  from dataclasses import dataclass
4
+ from typing import override
5
 
6
  from homeassistant.components.select import SelectEntity, SelectEntityDescription
7
  from homeassistant.const import STATE_ON, EntityCategory
8
  from homeassistant.core import HomeAssistant, callback
9
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
10
 
 
11
  from .const import (
 
 
 
 
 
12
  LOCATION,
 
 
 
13
  SELECT_DHW_MODE,
14
  SELECT_GATEWAY_MODE,
15
  SELECT_REGULATION_MODE,
16
  SELECT_SCHEDULE,
17
+ SELECT_ZONE_PROFILE,
18
  SelectOptionsType,
19
  SelectType,
20
  )
21
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
22
  from .entity import PlugwiseEntity
23
  from .util import plugwise_command
24
 
25
+ PARALLEL_UPDATES = 0
26
 
27
 
28
  @dataclass(frozen=True, kw_only=True)
 
33
  options_key: SelectOptionsType
34
 
35
 
 
36
  SELECT_TYPES = (
37
  PlugwiseSelectEntityDescription(
38
  key=SELECT_SCHEDULE,
39
  translation_key=SELECT_SCHEDULE,
40
+ options_key="available_schedules",
41
  ),
42
  PlugwiseSelectEntityDescription(
43
  key=SELECT_REGULATION_MODE,
44
+ translation_key=SELECT_REGULATION_MODE,
45
  entity_category=EntityCategory.CONFIG,
46
+ options_key="regulation_modes",
47
  ),
48
  PlugwiseSelectEntityDescription(
49
  key=SELECT_DHW_MODE,
50
+ translation_key=SELECT_DHW_MODE,
51
  entity_category=EntityCategory.CONFIG,
52
+ options_key="dhw_modes",
53
  ),
54
  PlugwiseSelectEntityDescription(
55
  key=SELECT_GATEWAY_MODE,
56
+ translation_key=SELECT_GATEWAY_MODE,
57
+ entity_category=EntityCategory.CONFIG,
58
+ options_key="gateway_modes",
59
+ ),
60
+ PlugwiseSelectEntityDescription(
61
+ key=SELECT_ZONE_PROFILE,
62
+ translation_key=SELECT_ZONE_PROFILE,
63
  entity_category=EntityCategory.CONFIG,
64
+ options_key="zone_profiles",
65
  ),
66
  )
67
 
 
69
  async def async_setup_entry(
70
  hass: HomeAssistant,
71
  entry: PlugwiseConfigEntry,
72
+ async_add_entities: AddConfigEntryEntitiesCallback,
73
  ) -> None:
74
+ """Set up the Smile selector from a config entry."""
75
  coordinator = entry.runtime_data
76
 
77
  @callback
 
80
  if not coordinator.new_devices:
81
  return
82
 
83
+ async_add_entities(
84
+ PlugwiseSelectEntity(coordinator, device_id, description)
85
+ for device_id in coordinator.new_devices
86
+ for description in SELECT_TYPES
87
+ if coordinator.data[device_id].get(description.options_key)
88
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  _add_entities()
91
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
107
  self._attr_unique_id = f"{device_id}-{entity_description.key}"
108
  self.entity_description = entity_description
109
 
110
+ self._device_or_location = device_id
111
+ if (
112
+ self.entity_description.key in (SELECT_SCHEDULE, SELECT_ZONE_PROFILE)
113
+ and (location := self.device.get(LOCATION)) is not None
114
+ ):
115
+ self._device_or_location = location
116
 
117
  @property
118
+ @override
119
+ def current_option(self) -> str | None:
120
  """Return the selected entity option to represent the entity state."""
121
  return self.device[self.entity_description.key]
122
 
123
  @property
124
+ @override
125
  def options(self) -> list[str]:
126
  """Return the available select-options."""
127
  return self.device[self.entity_description.options_key]
128
 
129
  @plugwise_command
130
+ @override
131
  async def async_select_option(self, option: str) -> None:
132
  """Change to the selected entity option.
133
 
134
+ The appliance ID (= device_id) is required for the dhw_mode select.
135
+ The location ID is required for the thermostat schedule and zone_profile selects.
136
+ STATE_ON is required for the thermostat schedule select.
137
  """
138
  await self.coordinator.api.set_select(
139
+ self.entity_description.key, self._device_or_location, option, STATE_ON
 
 
 
 
 
140
  )
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py RENAMED
@@ -1,8 +1,7 @@
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
 
@@ -13,13 +12,11 @@
13
  SensorStateClass,
14
  )
15
  from homeassistant.const import (
16
- ATTR_TEMPERATURE, # Upstream
17
  LIGHT_LUX,
18
  PERCENTAGE,
19
  EntityCategory,
20
  UnitOfElectricPotential,
21
  UnitOfEnergy,
22
- UnitOfIrradiance,
23
  UnitOfPower,
24
  UnitOfPressure,
25
  UnitOfTemperature,
@@ -27,64 +24,12 @@
27
  UnitOfVolumeFlowRate,
28
  )
29
  from homeassistant.core import HomeAssistant, callback
30
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
31
-
32
- from . import PlugwiseConfigEntry
33
- from .const import (
34
- DHW_SETPOINT,
35
- DHW_TEMP,
36
- EL_CONS_INTERVAL,
37
- EL_CONS_OP_CUMULATIVE,
38
- EL_CONS_OP_INTERVAL,
39
- EL_CONS_OP_POINT,
40
- EL_CONS_P_CUMULATIVE,
41
- EL_CONS_P_INTERVAL,
42
- EL_CONS_P_POINT,
43
- EL_CONS_POINT,
44
- EL_CONSUMED,
45
- EL_PH1_CONSUMED,
46
- EL_PH1_PRODUCED,
47
- EL_PH2_CONSUMED,
48
- EL_PH2_PRODUCED,
49
- EL_PH3_CONSUMED,
50
- EL_PH3_PRODUCED,
51
- EL_PROD_INTERVAL,
52
- EL_PROD_OP_CUMULATIVE,
53
- EL_PROD_OP_INTERVAL,
54
- EL_PROD_OP_POINT,
55
- EL_PROD_P_CUMULATIVE,
56
- EL_PROD_P_INTERVAL,
57
- EL_PROD_P_POINT,
58
- EL_PROD_POINT,
59
- EL_PRODUCED,
60
- GAS_CONS_CUMULATIVE,
61
- GAS_CONS_INTERVAL,
62
- INTENDED_BOILER_TEMP,
63
- LOGGER, # pw-beta
64
- MOD_LEVEL,
65
- NET_EL_CUMULATIVE,
66
- NET_EL_POINT,
67
- OUTDOOR_AIR_TEMP,
68
- OUTDOOR_TEMP,
69
- RETURN_TEMP,
70
- SENSORS,
71
- SOLAR_IRRADIANCE,
72
- TARGET_TEMP,
73
- TARGET_TEMP_HIGH,
74
- TARGET_TEMP_LOW,
75
- TEMP_DIFF,
76
- VALVE_POS,
77
- VOLTAGE_PH1,
78
- VOLTAGE_PH2,
79
- VOLTAGE_PH3,
80
- WATER_PRESSURE,
81
- WATER_TEMP,
82
- )
83
 
84
- # Upstream consts
85
- from .coordinator import PlugwiseDataUpdateCoordinator
86
  from .entity import PlugwiseEntity
87
 
 
88
  PARALLEL_UPDATES = 0
89
 
90
 
@@ -95,304 +40,299 @@
95
  key: SensorType
96
 
97
 
98
- # Upstream consts
99
- PLUGWISE_SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
100
  PlugwiseSensorEntityDescription(
101
- key=TARGET_TEMP,
102
- translation_key=TARGET_TEMP,
103
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
104
  device_class=SensorDeviceClass.TEMPERATURE,
105
  state_class=SensorStateClass.MEASUREMENT,
106
- entity_category=EntityCategory.DIAGNOSTIC,
107
  ),
108
  PlugwiseSensorEntityDescription(
109
- key=TARGET_TEMP_HIGH,
110
  translation_key="cooling_setpoint",
111
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
112
  device_class=SensorDeviceClass.TEMPERATURE,
113
  state_class=SensorStateClass.MEASUREMENT,
114
- entity_category=EntityCategory.DIAGNOSTIC,
115
  ),
116
  PlugwiseSensorEntityDescription(
117
- key=TARGET_TEMP_LOW,
118
  translation_key="heating_setpoint",
119
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
120
  device_class=SensorDeviceClass.TEMPERATURE,
121
  state_class=SensorStateClass.MEASUREMENT,
122
- entity_category=EntityCategory.DIAGNOSTIC,
123
  ),
124
  PlugwiseSensorEntityDescription(
125
- key=ATTR_TEMPERATURE,
126
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
127
  device_class=SensorDeviceClass.TEMPERATURE,
128
- entity_category=EntityCategory.DIAGNOSTIC,
129
  state_class=SensorStateClass.MEASUREMENT,
130
  ),
131
  PlugwiseSensorEntityDescription(
132
- key=INTENDED_BOILER_TEMP,
133
- translation_key=INTENDED_BOILER_TEMP,
134
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
135
  device_class=SensorDeviceClass.TEMPERATURE,
136
  entity_category=EntityCategory.DIAGNOSTIC,
137
  state_class=SensorStateClass.MEASUREMENT,
138
  ),
139
  PlugwiseSensorEntityDescription(
140
- key=TEMP_DIFF,
141
- translation_key=TEMP_DIFF,
142
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
143
  device_class=SensorDeviceClass.TEMPERATURE,
144
  entity_category=EntityCategory.DIAGNOSTIC,
145
  state_class=SensorStateClass.MEASUREMENT,
146
  ),
147
  PlugwiseSensorEntityDescription(
148
- key=OUTDOOR_TEMP,
149
- translation_key=OUTDOOR_TEMP,
150
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
151
  device_class=SensorDeviceClass.TEMPERATURE,
 
152
  state_class=SensorStateClass.MEASUREMENT,
153
- suggested_display_precision=1,
154
  ),
155
  PlugwiseSensorEntityDescription(
156
- key=OUTDOOR_AIR_TEMP,
157
- translation_key=OUTDOOR_AIR_TEMP,
158
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
159
  device_class=SensorDeviceClass.TEMPERATURE,
160
  entity_category=EntityCategory.DIAGNOSTIC,
161
  state_class=SensorStateClass.MEASUREMENT,
162
  ),
163
  PlugwiseSensorEntityDescription(
164
- key=WATER_TEMP,
165
- translation_key=WATER_TEMP,
166
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
167
  device_class=SensorDeviceClass.TEMPERATURE,
168
  entity_category=EntityCategory.DIAGNOSTIC,
169
  state_class=SensorStateClass.MEASUREMENT,
170
  ),
171
  PlugwiseSensorEntityDescription(
172
- key=RETURN_TEMP,
173
- translation_key=RETURN_TEMP,
174
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
175
  device_class=SensorDeviceClass.TEMPERATURE,
176
  entity_category=EntityCategory.DIAGNOSTIC,
177
  state_class=SensorStateClass.MEASUREMENT,
178
  ),
179
  PlugwiseSensorEntityDescription(
180
- key=EL_CONSUMED,
181
- translation_key=EL_CONSUMED,
182
  native_unit_of_measurement=UnitOfPower.WATT,
183
  device_class=SensorDeviceClass.POWER,
184
  state_class=SensorStateClass.MEASUREMENT,
185
  ),
186
  PlugwiseSensorEntityDescription(
187
- key=EL_PRODUCED,
188
- translation_key=EL_PRODUCED,
189
  native_unit_of_measurement=UnitOfPower.WATT,
190
  device_class=SensorDeviceClass.POWER,
191
  state_class=SensorStateClass.MEASUREMENT,
192
  entity_registry_enabled_default=False,
193
  ),
194
  PlugwiseSensorEntityDescription(
195
- key=EL_CONS_INTERVAL,
196
- translation_key=EL_CONS_INTERVAL,
197
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
198
  device_class=SensorDeviceClass.ENERGY,
199
  state_class=SensorStateClass.TOTAL,
200
  ),
201
  PlugwiseSensorEntityDescription(
202
- key=EL_CONS_P_INTERVAL,
203
- translation_key=EL_CONS_P_INTERVAL,
204
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
205
  device_class=SensorDeviceClass.ENERGY,
206
  state_class=SensorStateClass.TOTAL,
207
  ),
208
  PlugwiseSensorEntityDescription(
209
- key=EL_CONS_OP_INTERVAL,
210
- translation_key=EL_CONS_OP_INTERVAL,
211
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
212
  device_class=SensorDeviceClass.ENERGY,
213
  state_class=SensorStateClass.TOTAL,
214
  ),
215
  PlugwiseSensorEntityDescription(
216
- key=EL_PROD_INTERVAL,
217
- translation_key=EL_PROD_INTERVAL,
218
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
219
  device_class=SensorDeviceClass.ENERGY,
220
  state_class=SensorStateClass.TOTAL,
221
  entity_registry_enabled_default=False,
222
  ),
223
  PlugwiseSensorEntityDescription(
224
- key=EL_PROD_P_INTERVAL,
225
- translation_key=EL_PROD_P_INTERVAL,
226
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
227
  device_class=SensorDeviceClass.ENERGY,
228
  state_class=SensorStateClass.TOTAL,
229
  ),
230
  PlugwiseSensorEntityDescription(
231
- key=EL_PROD_OP_INTERVAL,
232
- translation_key=EL_PROD_OP_INTERVAL,
233
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
234
  device_class=SensorDeviceClass.ENERGY,
235
  state_class=SensorStateClass.TOTAL,
236
  ),
237
  PlugwiseSensorEntityDescription(
238
- key=EL_CONS_POINT,
239
- translation_key=EL_CONS_POINT,
240
  device_class=SensorDeviceClass.POWER,
241
  native_unit_of_measurement=UnitOfPower.WATT,
242
  state_class=SensorStateClass.MEASUREMENT,
243
  ),
244
  PlugwiseSensorEntityDescription(
245
- key=EL_CONS_OP_POINT,
246
- translation_key=EL_CONS_OP_POINT,
247
  native_unit_of_measurement=UnitOfPower.WATT,
248
  device_class=SensorDeviceClass.POWER,
249
  state_class=SensorStateClass.MEASUREMENT,
250
  ),
251
  PlugwiseSensorEntityDescription(
252
- key=EL_CONS_P_POINT,
253
- translation_key=EL_CONS_P_POINT,
254
  native_unit_of_measurement=UnitOfPower.WATT,
255
  device_class=SensorDeviceClass.POWER,
256
  state_class=SensorStateClass.MEASUREMENT,
257
  ),
258
  PlugwiseSensorEntityDescription(
259
- key=EL_CONS_OP_CUMULATIVE,
260
- translation_key=EL_CONS_OP_CUMULATIVE,
261
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
262
  device_class=SensorDeviceClass.ENERGY,
263
  state_class=SensorStateClass.TOTAL_INCREASING,
264
  ),
265
  PlugwiseSensorEntityDescription(
266
- key=EL_CONS_P_CUMULATIVE,
267
- translation_key=EL_CONS_P_CUMULATIVE,
268
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
269
  device_class=SensorDeviceClass.ENERGY,
270
  state_class=SensorStateClass.TOTAL_INCREASING,
271
  ),
272
  PlugwiseSensorEntityDescription(
273
- key=EL_PROD_POINT,
274
- translation_key=EL_PROD_POINT,
275
  device_class=SensorDeviceClass.POWER,
276
  native_unit_of_measurement=UnitOfPower.WATT,
277
  state_class=SensorStateClass.MEASUREMENT,
278
  ),
279
  PlugwiseSensorEntityDescription(
280
- key=EL_PROD_OP_POINT,
281
- translation_key=EL_PROD_OP_POINT,
282
  native_unit_of_measurement=UnitOfPower.WATT,
283
  device_class=SensorDeviceClass.POWER,
284
  state_class=SensorStateClass.MEASUREMENT,
285
  ),
286
  PlugwiseSensorEntityDescription(
287
- key=EL_PROD_P_POINT,
288
- translation_key=EL_PROD_P_POINT,
289
  native_unit_of_measurement=UnitOfPower.WATT,
290
  device_class=SensorDeviceClass.POWER,
291
  state_class=SensorStateClass.MEASUREMENT,
292
  ),
293
  PlugwiseSensorEntityDescription(
294
- key=EL_PROD_OP_CUMULATIVE,
295
- translation_key=EL_PROD_OP_CUMULATIVE,
296
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
297
  device_class=SensorDeviceClass.ENERGY,
298
  state_class=SensorStateClass.TOTAL_INCREASING,
299
  ),
300
  PlugwiseSensorEntityDescription(
301
- key=EL_PROD_P_CUMULATIVE,
302
- translation_key=EL_PROD_P_CUMULATIVE,
303
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
304
  device_class=SensorDeviceClass.ENERGY,
305
  state_class=SensorStateClass.TOTAL_INCREASING,
306
  ),
307
  PlugwiseSensorEntityDescription(
308
- key=EL_PH1_CONSUMED,
309
- translation_key=EL_PH1_CONSUMED,
310
  device_class=SensorDeviceClass.POWER,
311
  native_unit_of_measurement=UnitOfPower.WATT,
312
  state_class=SensorStateClass.MEASUREMENT,
313
  ),
314
  PlugwiseSensorEntityDescription(
315
- key=EL_PH2_CONSUMED,
316
- translation_key=EL_PH2_CONSUMED,
317
  device_class=SensorDeviceClass.POWER,
318
  native_unit_of_measurement=UnitOfPower.WATT,
319
  state_class=SensorStateClass.MEASUREMENT,
320
  ),
321
  PlugwiseSensorEntityDescription(
322
- key=EL_PH3_CONSUMED,
323
- translation_key=EL_PH3_CONSUMED,
324
  device_class=SensorDeviceClass.POWER,
325
  native_unit_of_measurement=UnitOfPower.WATT,
326
  state_class=SensorStateClass.MEASUREMENT,
327
  ),
328
  PlugwiseSensorEntityDescription(
329
- key=EL_PH1_PRODUCED,
330
- translation_key=EL_PH1_PRODUCED,
331
  device_class=SensorDeviceClass.POWER,
332
  native_unit_of_measurement=UnitOfPower.WATT,
333
  state_class=SensorStateClass.MEASUREMENT,
334
  ),
335
  PlugwiseSensorEntityDescription(
336
- key=EL_PH2_PRODUCED,
337
- translation_key=EL_PH2_PRODUCED,
338
  device_class=SensorDeviceClass.POWER,
339
  native_unit_of_measurement=UnitOfPower.WATT,
340
  state_class=SensorStateClass.MEASUREMENT,
341
  ),
342
  PlugwiseSensorEntityDescription(
343
- key=EL_PH3_PRODUCED,
344
- translation_key=EL_PH3_PRODUCED,
345
  device_class=SensorDeviceClass.POWER,
346
  native_unit_of_measurement=UnitOfPower.WATT,
347
  state_class=SensorStateClass.MEASUREMENT,
348
  ),
349
  PlugwiseSensorEntityDescription(
350
- key=VOLTAGE_PH1,
351
- translation_key=VOLTAGE_PH1,
352
  device_class=SensorDeviceClass.VOLTAGE,
353
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
354
  state_class=SensorStateClass.MEASUREMENT,
355
  entity_registry_enabled_default=False,
356
  ),
357
  PlugwiseSensorEntityDescription(
358
- key=VOLTAGE_PH2,
359
- translation_key=VOLTAGE_PH2,
360
  device_class=SensorDeviceClass.VOLTAGE,
361
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
362
  state_class=SensorStateClass.MEASUREMENT,
363
  entity_registry_enabled_default=False,
364
  ),
365
  PlugwiseSensorEntityDescription(
366
- key=VOLTAGE_PH3,
367
- translation_key=VOLTAGE_PH3,
368
  device_class=SensorDeviceClass.VOLTAGE,
369
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
370
  state_class=SensorStateClass.MEASUREMENT,
371
  entity_registry_enabled_default=False,
372
  ),
373
  PlugwiseSensorEntityDescription(
374
- key=GAS_CONS_INTERVAL,
375
- translation_key=GAS_CONS_INTERVAL,
376
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
377
  state_class=SensorStateClass.MEASUREMENT,
378
  ),
379
  PlugwiseSensorEntityDescription(
380
- key=GAS_CONS_CUMULATIVE,
381
- translation_key=GAS_CONS_CUMULATIVE,
382
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
383
  device_class=SensorDeviceClass.GAS,
384
  state_class=SensorStateClass.TOTAL,
385
  ),
386
  PlugwiseSensorEntityDescription(
387
- key=NET_EL_POINT,
388
- translation_key=NET_EL_POINT,
389
  native_unit_of_measurement=UnitOfPower.WATT,
390
  device_class=SensorDeviceClass.POWER,
391
  state_class=SensorStateClass.MEASUREMENT,
392
  ),
393
  PlugwiseSensorEntityDescription(
394
- key=NET_EL_CUMULATIVE,
395
- translation_key=NET_EL_CUMULATIVE,
396
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
397
  device_class=SensorDeviceClass.ENERGY,
398
  state_class=SensorStateClass.TOTAL,
@@ -408,26 +348,26 @@
408
  key="illuminance",
409
  native_unit_of_measurement=LIGHT_LUX,
410
  device_class=SensorDeviceClass.ILLUMINANCE,
411
- state_class=SensorStateClass.MEASUREMENT,
412
  entity_category=EntityCategory.DIAGNOSTIC,
 
413
  ),
414
  PlugwiseSensorEntityDescription(
415
- key=MOD_LEVEL,
416
- translation_key=MOD_LEVEL,
417
  native_unit_of_measurement=PERCENTAGE,
418
  entity_category=EntityCategory.DIAGNOSTIC,
419
  state_class=SensorStateClass.MEASUREMENT,
420
  ),
421
  PlugwiseSensorEntityDescription(
422
- key=VALVE_POS,
423
- translation_key=VALVE_POS,
424
- entity_category=EntityCategory.DIAGNOSTIC,
425
  native_unit_of_measurement=PERCENTAGE,
 
426
  state_class=SensorStateClass.MEASUREMENT,
427
  ),
428
  PlugwiseSensorEntityDescription(
429
- key=WATER_PRESSURE,
430
- translation_key=WATER_PRESSURE,
431
  native_unit_of_measurement=UnitOfPressure.BAR,
432
  device_class=SensorDeviceClass.PRESSURE,
433
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -440,22 +380,16 @@
440
  state_class=SensorStateClass.MEASUREMENT,
441
  ),
442
  PlugwiseSensorEntityDescription(
443
- key=SOLAR_IRRADIANCE,
444
- native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
445
- device_class=SensorDeviceClass.IRRADIANCE,
446
- state_class=SensorStateClass.MEASUREMENT,
447
- ),
448
- PlugwiseSensorEntityDescription(
449
- key=DHW_TEMP,
450
- translation_key=DHW_TEMP,
451
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
452
  device_class=SensorDeviceClass.TEMPERATURE,
453
  entity_category=EntityCategory.DIAGNOSTIC,
454
  state_class=SensorStateClass.MEASUREMENT,
455
  ),
456
  PlugwiseSensorEntityDescription(
457
- key=DHW_SETPOINT,
458
- translation_key=DHW_SETPOINT,
459
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
460
  device_class=SensorDeviceClass.TEMPERATURE,
461
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -467,10 +401,9 @@
467
  async def async_setup_entry(
468
  hass: HomeAssistant,
469
  entry: PlugwiseConfigEntry,
470
- async_add_entities: AddEntitiesCallback,
471
  ) -> None:
472
- """Set up Plugwise sensors from a config entry."""
473
- # Upstream as Plugwise not Smile
474
  coordinator = entry.runtime_data
475
 
476
  @callback
@@ -479,29 +412,13 @@
479
  if not coordinator.new_devices:
480
  return
481
 
482
- # Upstream consts
483
- # async_add_entities(
484
- # PlugwiseSensorEntity(coordinator, device_id, description)
485
- # for device_id in coordinator.new_devices
486
- # if (sensors := coordinator.data.devices[device_id].get(SENSORS))
487
- # for description in PLUGWISE_SENSORS
488
- # if description.key in sensors
489
- # )
490
- # pw-beta alternative for debugging
491
- entities: list[PlugwiseSensorEntity] = []
492
- for device_id in coordinator.new_devices:
493
- device = coordinator.data.devices[device_id]
494
- if not (sensors := device.get(SENSORS)):
495
- continue
496
- for description in PLUGWISE_SENSORS:
497
- if description.key not in sensors:
498
- continue
499
- entities.append(PlugwiseSensorEntity(coordinator, device_id, description))
500
- LOGGER.debug(
501
- "Add %s %s sensor", device["name"], description.translation_key or description.key
502
- )
503
-
504
- async_add_entities(entities)
505
 
506
  _add_entities()
507
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -520,10 +437,11 @@
520
  ) -> None:
521
  """Initialise the sensor."""
522
  super().__init__(coordinator, device_id)
523
- self.entity_description = description
524
  self._attr_unique_id = f"{device_id}-{description.key}"
 
525
 
526
  @property
 
527
  def native_value(self) -> int | float:
528
  """Return the value reported by the sensor."""
529
- return self.device[SENSORS][self.entity_description.key] # Upstream consts
 
1
  """Plugwise Sensor component for Home Assistant."""
2
 
 
 
3
  from dataclasses import dataclass
4
+ from typing import override
5
 
6
  from plugwise.constants import SensorType
7
 
 
12
  SensorStateClass,
13
  )
14
  from homeassistant.const import (
 
15
  LIGHT_LUX,
16
  PERCENTAGE,
17
  EntityCategory,
18
  UnitOfElectricPotential,
19
  UnitOfEnergy,
 
20
  UnitOfPower,
21
  UnitOfPressure,
22
  UnitOfTemperature,
 
24
  UnitOfVolumeFlowRate,
25
  )
26
  from homeassistant.core import HomeAssistant, callback
27
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
30
  from .entity import PlugwiseEntity
31
 
32
+ # Coordinator is used to centralize the data updates
33
  PARALLEL_UPDATES = 0
34
 
35
 
 
40
  key: SensorType
41
 
42
 
43
+ SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
 
44
  PlugwiseSensorEntityDescription(
45
+ key="setpoint",
46
+ translation_key="setpoint",
47
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
48
  device_class=SensorDeviceClass.TEMPERATURE,
49
  state_class=SensorStateClass.MEASUREMENT,
 
50
  ),
51
  PlugwiseSensorEntityDescription(
52
+ key="setpoint_high",
53
  translation_key="cooling_setpoint",
54
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
55
  device_class=SensorDeviceClass.TEMPERATURE,
56
  state_class=SensorStateClass.MEASUREMENT,
 
57
  ),
58
  PlugwiseSensorEntityDescription(
59
+ key="setpoint_low",
60
  translation_key="heating_setpoint",
61
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
62
  device_class=SensorDeviceClass.TEMPERATURE,
63
  state_class=SensorStateClass.MEASUREMENT,
 
64
  ),
65
  PlugwiseSensorEntityDescription(
66
+ key="temperature",
67
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
68
  device_class=SensorDeviceClass.TEMPERATURE,
 
69
  state_class=SensorStateClass.MEASUREMENT,
70
  ),
71
  PlugwiseSensorEntityDescription(
72
+ key="intended_boiler_temperature",
73
+ translation_key="intended_boiler_temperature",
74
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
75
  device_class=SensorDeviceClass.TEMPERATURE,
76
  entity_category=EntityCategory.DIAGNOSTIC,
77
  state_class=SensorStateClass.MEASUREMENT,
78
  ),
79
  PlugwiseSensorEntityDescription(
80
+ key="temperature_difference",
81
+ translation_key="temperature_difference",
82
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
83
  device_class=SensorDeviceClass.TEMPERATURE,
84
  entity_category=EntityCategory.DIAGNOSTIC,
85
  state_class=SensorStateClass.MEASUREMENT,
86
  ),
87
  PlugwiseSensorEntityDescription(
88
+ key="outdoor_temperature",
89
+ translation_key="outdoor_temperature",
90
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
91
  device_class=SensorDeviceClass.TEMPERATURE,
92
+ entity_category=EntityCategory.DIAGNOSTIC,
93
  state_class=SensorStateClass.MEASUREMENT,
 
94
  ),
95
  PlugwiseSensorEntityDescription(
96
+ key="outdoor_air_temperature",
97
+ translation_key="outdoor_air_temperature",
98
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
99
  device_class=SensorDeviceClass.TEMPERATURE,
100
  entity_category=EntityCategory.DIAGNOSTIC,
101
  state_class=SensorStateClass.MEASUREMENT,
102
  ),
103
  PlugwiseSensorEntityDescription(
104
+ key="water_temperature",
105
+ translation_key="water_temperature",
106
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
107
  device_class=SensorDeviceClass.TEMPERATURE,
108
  entity_category=EntityCategory.DIAGNOSTIC,
109
  state_class=SensorStateClass.MEASUREMENT,
110
  ),
111
  PlugwiseSensorEntityDescription(
112
+ key="return_temperature",
113
+ translation_key="return_temperature",
114
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
115
  device_class=SensorDeviceClass.TEMPERATURE,
116
  entity_category=EntityCategory.DIAGNOSTIC,
117
  state_class=SensorStateClass.MEASUREMENT,
118
  ),
119
  PlugwiseSensorEntityDescription(
120
+ key="electricity_consumed",
121
+ translation_key="electricity_consumed",
122
  native_unit_of_measurement=UnitOfPower.WATT,
123
  device_class=SensorDeviceClass.POWER,
124
  state_class=SensorStateClass.MEASUREMENT,
125
  ),
126
  PlugwiseSensorEntityDescription(
127
+ key="electricity_produced",
128
+ translation_key="electricity_produced",
129
  native_unit_of_measurement=UnitOfPower.WATT,
130
  device_class=SensorDeviceClass.POWER,
131
  state_class=SensorStateClass.MEASUREMENT,
132
  entity_registry_enabled_default=False,
133
  ),
134
  PlugwiseSensorEntityDescription(
135
+ key="electricity_consumed_interval",
136
+ translation_key="electricity_consumed_interval",
137
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
138
  device_class=SensorDeviceClass.ENERGY,
139
  state_class=SensorStateClass.TOTAL,
140
  ),
141
  PlugwiseSensorEntityDescription(
142
+ key="electricity_consumed_peak_interval",
143
+ translation_key="electricity_consumed_peak_interval",
144
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
145
  device_class=SensorDeviceClass.ENERGY,
146
  state_class=SensorStateClass.TOTAL,
147
  ),
148
  PlugwiseSensorEntityDescription(
149
+ key="electricity_consumed_off_peak_interval",
150
+ translation_key="electricity_consumed_off_peak_interval",
151
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
152
  device_class=SensorDeviceClass.ENERGY,
153
  state_class=SensorStateClass.TOTAL,
154
  ),
155
  PlugwiseSensorEntityDescription(
156
+ key="electricity_produced_interval",
157
+ translation_key="electricity_produced_interval",
158
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
159
  device_class=SensorDeviceClass.ENERGY,
160
  state_class=SensorStateClass.TOTAL,
161
  entity_registry_enabled_default=False,
162
  ),
163
  PlugwiseSensorEntityDescription(
164
+ key="electricity_produced_peak_interval",
165
+ translation_key="electricity_produced_peak_interval",
166
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
167
  device_class=SensorDeviceClass.ENERGY,
168
  state_class=SensorStateClass.TOTAL,
169
  ),
170
  PlugwiseSensorEntityDescription(
171
+ key="electricity_produced_off_peak_interval",
172
+ translation_key="electricity_produced_off_peak_interval",
173
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
174
  device_class=SensorDeviceClass.ENERGY,
175
  state_class=SensorStateClass.TOTAL,
176
  ),
177
  PlugwiseSensorEntityDescription(
178
+ key="electricity_consumed_point",
179
+ translation_key="electricity_consumed_point",
180
  device_class=SensorDeviceClass.POWER,
181
  native_unit_of_measurement=UnitOfPower.WATT,
182
  state_class=SensorStateClass.MEASUREMENT,
183
  ),
184
  PlugwiseSensorEntityDescription(
185
+ key="electricity_consumed_off_peak_point",
186
+ translation_key="electricity_consumed_off_peak_point",
187
  native_unit_of_measurement=UnitOfPower.WATT,
188
  device_class=SensorDeviceClass.POWER,
189
  state_class=SensorStateClass.MEASUREMENT,
190
  ),
191
  PlugwiseSensorEntityDescription(
192
+ key="electricity_consumed_peak_point",
193
+ translation_key="electricity_consumed_peak_point",
194
  native_unit_of_measurement=UnitOfPower.WATT,
195
  device_class=SensorDeviceClass.POWER,
196
  state_class=SensorStateClass.MEASUREMENT,
197
  ),
198
  PlugwiseSensorEntityDescription(
199
+ key="electricity_consumed_off_peak_cumulative",
200
+ translation_key="electricity_consumed_off_peak_cumulative",
201
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
202
  device_class=SensorDeviceClass.ENERGY,
203
  state_class=SensorStateClass.TOTAL_INCREASING,
204
  ),
205
  PlugwiseSensorEntityDescription(
206
+ key="electricity_consumed_peak_cumulative",
207
+ translation_key="electricity_consumed_peak_cumulative",
208
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
209
  device_class=SensorDeviceClass.ENERGY,
210
  state_class=SensorStateClass.TOTAL_INCREASING,
211
  ),
212
  PlugwiseSensorEntityDescription(
213
+ key="electricity_produced_point",
214
+ translation_key="electricity_produced_point",
215
  device_class=SensorDeviceClass.POWER,
216
  native_unit_of_measurement=UnitOfPower.WATT,
217
  state_class=SensorStateClass.MEASUREMENT,
218
  ),
219
  PlugwiseSensorEntityDescription(
220
+ key="electricity_produced_off_peak_point",
221
+ translation_key="electricity_produced_off_peak_point",
222
  native_unit_of_measurement=UnitOfPower.WATT,
223
  device_class=SensorDeviceClass.POWER,
224
  state_class=SensorStateClass.MEASUREMENT,
225
  ),
226
  PlugwiseSensorEntityDescription(
227
+ key="electricity_produced_peak_point",
228
+ translation_key="electricity_produced_peak_point",
229
  native_unit_of_measurement=UnitOfPower.WATT,
230
  device_class=SensorDeviceClass.POWER,
231
  state_class=SensorStateClass.MEASUREMENT,
232
  ),
233
  PlugwiseSensorEntityDescription(
234
+ key="electricity_produced_off_peak_cumulative",
235
+ translation_key="electricity_produced_off_peak_cumulative",
236
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
237
  device_class=SensorDeviceClass.ENERGY,
238
  state_class=SensorStateClass.TOTAL_INCREASING,
239
  ),
240
  PlugwiseSensorEntityDescription(
241
+ key="electricity_produced_peak_cumulative",
242
+ translation_key="electricity_produced_peak_cumulative",
243
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
244
  device_class=SensorDeviceClass.ENERGY,
245
  state_class=SensorStateClass.TOTAL_INCREASING,
246
  ),
247
  PlugwiseSensorEntityDescription(
248
+ key="electricity_phase_one_consumed",
249
+ translation_key="electricity_phase_one_consumed",
250
  device_class=SensorDeviceClass.POWER,
251
  native_unit_of_measurement=UnitOfPower.WATT,
252
  state_class=SensorStateClass.MEASUREMENT,
253
  ),
254
  PlugwiseSensorEntityDescription(
255
+ key="electricity_phase_two_consumed",
256
+ translation_key="electricity_phase_two_consumed",
257
  device_class=SensorDeviceClass.POWER,
258
  native_unit_of_measurement=UnitOfPower.WATT,
259
  state_class=SensorStateClass.MEASUREMENT,
260
  ),
261
  PlugwiseSensorEntityDescription(
262
+ key="electricity_phase_three_consumed",
263
+ translation_key="electricity_phase_three_consumed",
264
  device_class=SensorDeviceClass.POWER,
265
  native_unit_of_measurement=UnitOfPower.WATT,
266
  state_class=SensorStateClass.MEASUREMENT,
267
  ),
268
  PlugwiseSensorEntityDescription(
269
+ key="electricity_phase_one_produced",
270
+ translation_key="electricity_phase_one_produced",
271
  device_class=SensorDeviceClass.POWER,
272
  native_unit_of_measurement=UnitOfPower.WATT,
273
  state_class=SensorStateClass.MEASUREMENT,
274
  ),
275
  PlugwiseSensorEntityDescription(
276
+ key="electricity_phase_two_produced",
277
+ translation_key="electricity_phase_two_produced",
278
  device_class=SensorDeviceClass.POWER,
279
  native_unit_of_measurement=UnitOfPower.WATT,
280
  state_class=SensorStateClass.MEASUREMENT,
281
  ),
282
  PlugwiseSensorEntityDescription(
283
+ key="electricity_phase_three_produced",
284
+ translation_key="electricity_phase_three_produced",
285
  device_class=SensorDeviceClass.POWER,
286
  native_unit_of_measurement=UnitOfPower.WATT,
287
  state_class=SensorStateClass.MEASUREMENT,
288
  ),
289
  PlugwiseSensorEntityDescription(
290
+ key="voltage_phase_one",
291
+ translation_key="voltage_phase_one",
292
  device_class=SensorDeviceClass.VOLTAGE,
293
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
294
  state_class=SensorStateClass.MEASUREMENT,
295
  entity_registry_enabled_default=False,
296
  ),
297
  PlugwiseSensorEntityDescription(
298
+ key="voltage_phase_two",
299
+ translation_key="voltage_phase_two",
300
  device_class=SensorDeviceClass.VOLTAGE,
301
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
302
  state_class=SensorStateClass.MEASUREMENT,
303
  entity_registry_enabled_default=False,
304
  ),
305
  PlugwiseSensorEntityDescription(
306
+ key="voltage_phase_three",
307
+ translation_key="voltage_phase_three",
308
  device_class=SensorDeviceClass.VOLTAGE,
309
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
310
  state_class=SensorStateClass.MEASUREMENT,
311
  entity_registry_enabled_default=False,
312
  ),
313
  PlugwiseSensorEntityDescription(
314
+ key="gas_consumed_interval",
315
+ translation_key="gas_consumed_interval",
316
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
317
  state_class=SensorStateClass.MEASUREMENT,
318
  ),
319
  PlugwiseSensorEntityDescription(
320
+ key="gas_consumed_cumulative",
321
+ translation_key="gas_consumed_cumulative",
322
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
323
  device_class=SensorDeviceClass.GAS,
324
  state_class=SensorStateClass.TOTAL,
325
  ),
326
  PlugwiseSensorEntityDescription(
327
+ key="net_electricity_point",
328
+ translation_key="net_electricity_point",
329
  native_unit_of_measurement=UnitOfPower.WATT,
330
  device_class=SensorDeviceClass.POWER,
331
  state_class=SensorStateClass.MEASUREMENT,
332
  ),
333
  PlugwiseSensorEntityDescription(
334
+ key="net_electricity_cumulative",
335
+ translation_key="net_electricity_cumulative",
336
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
337
  device_class=SensorDeviceClass.ENERGY,
338
  state_class=SensorStateClass.TOTAL,
 
348
  key="illuminance",
349
  native_unit_of_measurement=LIGHT_LUX,
350
  device_class=SensorDeviceClass.ILLUMINANCE,
 
351
  entity_category=EntityCategory.DIAGNOSTIC,
352
+ state_class=SensorStateClass.MEASUREMENT,
353
  ),
354
  PlugwiseSensorEntityDescription(
355
+ key="modulation_level",
356
+ translation_key="modulation_level",
357
  native_unit_of_measurement=PERCENTAGE,
358
  entity_category=EntityCategory.DIAGNOSTIC,
359
  state_class=SensorStateClass.MEASUREMENT,
360
  ),
361
  PlugwiseSensorEntityDescription(
362
+ key="valve_position",
363
+ translation_key="valve_position",
 
364
  native_unit_of_measurement=PERCENTAGE,
365
+ entity_category=EntityCategory.DIAGNOSTIC,
366
  state_class=SensorStateClass.MEASUREMENT,
367
  ),
368
  PlugwiseSensorEntityDescription(
369
+ key="water_pressure",
370
+ translation_key="water_pressure",
371
  native_unit_of_measurement=UnitOfPressure.BAR,
372
  device_class=SensorDeviceClass.PRESSURE,
373
  entity_category=EntityCategory.DIAGNOSTIC,
 
380
  state_class=SensorStateClass.MEASUREMENT,
381
  ),
382
  PlugwiseSensorEntityDescription(
383
+ key="dhw_temperature",
384
+ translation_key="dhw_temperature",
 
 
 
 
 
 
385
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
386
  device_class=SensorDeviceClass.TEMPERATURE,
387
  entity_category=EntityCategory.DIAGNOSTIC,
388
  state_class=SensorStateClass.MEASUREMENT,
389
  ),
390
  PlugwiseSensorEntityDescription(
391
+ key="domestic_hot_water_setpoint",
392
+ translation_key="domestic_hot_water_setpoint",
393
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
394
  device_class=SensorDeviceClass.TEMPERATURE,
395
  entity_category=EntityCategory.DIAGNOSTIC,
 
401
  async def async_setup_entry(
402
  hass: HomeAssistant,
403
  entry: PlugwiseConfigEntry,
404
+ async_add_entities: AddConfigEntryEntitiesCallback,
405
  ) -> None:
406
+ """Set up the Smile sensors from a config entry."""
 
407
  coordinator = entry.runtime_data
408
 
409
  @callback
 
412
  if not coordinator.new_devices:
413
  return
414
 
415
+ async_add_entities(
416
+ PlugwiseSensorEntity(coordinator, device_id, description)
417
+ for device_id in coordinator.new_devices
418
+ if (sensors := coordinator.data[device_id].get("sensors"))
419
+ for description in SENSORS
420
+ if description.key in sensors
421
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
  _add_entities()
424
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
437
  ) -> None:
438
  """Initialise the sensor."""
439
  super().__init__(coordinator, device_id)
 
440
  self._attr_unique_id = f"{device_id}-{description.key}"
441
+ self.entity_description = description
442
 
443
  @property
444
+ @override
445
  def native_value(self) -> int | float:
446
  """Return the value reported by the sensor."""
447
+ return self.device["sensors"][self.entity_description.key]
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/strings.json RENAMED
@@ -1,61 +1,60 @@
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": "Set up Plugwise Adam/Smile/Stretch",
23
- "description": "Enter your Plugwise device: (setup can take up to 90s)",
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": {
49
  "binary_sensor": {
50
- "low_battery": {
51
- "name": "Battery state"
52
- },
53
  "compressor_state": {
54
  "name": "Compressor state"
55
  },
56
  "cooling_enabled": {
57
  "name": "Cooling enabled"
58
  },
 
 
 
59
  "dhw_state": {
60
  "name": "DHW state"
61
  },
@@ -63,16 +62,13 @@
63
  "name": "Flame state"
64
  },
65
  "heating_state": {
66
- "name": "Heating"
67
  },
68
- "cooling_state": {
69
- "name": "Cooling"
70
  },
71
  "secondary_boiler_state": {
72
  "name": "Secondary boiler state"
73
- },
74
- "plugwise_notification": {
75
- "name": "Plugwise notification"
76
  }
77
  },
78
  "button": {
@@ -83,223 +79,257 @@
83
  "climate": {
84
  "plugwise": {
85
  "state_attributes": {
 
 
 
86
  "preset_mode": {
87
  "state": {
88
  "asleep": "Night",
89
- "away": "Away",
90
- "home": "Home",
91
  "no_frost": "Anti-frost",
92
  "vacation": "Vacation"
93
  }
 
 
 
94
  }
95
  }
96
  }
97
  },
98
  "number": {
99
- "maximum_boiler_temperature": {
100
- "name": "Maximum boiler temperature setpoint"
101
- },
102
  "max_dhw_temperature": {
103
  "name": "Domestic hot water setpoint"
104
  },
 
 
 
105
  "temperature_offset": {
106
  "name": "Temperature offset"
107
  }
108
  },
109
  "select": {
110
- "dhw_mode": {
111
  "name": "DHW mode",
112
  "state": {
113
- "auto": "Auto",
114
- "boost": "Boost",
115
- "comfort": "Comfort",
116
- "off": "Off"
 
 
 
 
 
 
 
 
117
  }
118
  },
119
- "regulation_mode": {
120
  "name": "Regulation mode",
121
  "state": {
122
  "bleeding_cold": "Bleeding cold",
123
  "bleeding_hot": "Bleeding hot",
124
- "cooling": "Cooling",
125
- "heating": "Heating",
126
- "off": "Off"
127
  }
128
  },
129
- "gateway_mode": {
130
- "name": "Gateway mode",
131
  "state": {
132
- "away": "Pause",
133
- "full": "Normal",
134
- "vacation": "Vacation"
135
  }
136
  },
137
- "select_schedule": {
138
- "name": "Thermostat schedule",
139
  "state": {
140
- "off": "Off"
 
 
141
  }
142
  }
143
  },
144
  "sensor": {
145
- "setpoint": {
146
- "name": "Setpoint"
147
- },
148
  "cooling_setpoint": {
149
  "name": "Cooling setpoint"
150
  },
151
- "heating_setpoint": {
152
- "name": "Heating setpoint"
153
- },
154
- "intended_boiler_temperature": {
155
- "name": "Intended boiler temperature"
156
- },
157
- "temperature_difference": {
158
- "name": "Temperature difference"
159
- },
160
- "outdoor_temperature": {
161
- "name": "Outdoor temperature"
162
- },
163
- "outdoor_air_temperature": {
164
- "name": "Outdoor air temperature"
165
- },
166
- "water_temperature": {
167
- "name": "Water temperature"
168
  },
169
- "return_temperature": {
170
- "name": "Return temperature"
171
  },
172
  "electricity_consumed": {
173
  "name": "Electricity consumed"
174
  },
175
- "electricity_produced": {
176
- "name": "Electricity produced"
177
- },
178
- "electricity_consumed_point": {
179
- "name": "Electricity consumed point"
180
- },
181
- "electricity_produced_point": {
182
- "name": "Electricity produced point"
183
- },
184
  "electricity_consumed_interval": {
185
  "name": "Electricity consumed interval"
186
  },
187
- "electricity_consumed_peak_interval": {
188
- "name": "Electricity consumed peak interval"
189
  },
190
  "electricity_consumed_off_peak_interval": {
191
- "name": "Electricity consumed off peak interval"
192
- },
193
- "electricity_produced_interval": {
194
- "name": "Electricity produced interval"
195
- },
196
- "electricity_produced_peak_interval": {
197
- "name": "Electricity produced peak interval"
198
- },
199
- "electricity_produced_off_peak_interval": {
200
- "name": "Electricity produced off peak interval"
201
  },
202
  "electricity_consumed_off_peak_point": {
203
- "name": "Electricity consumed off peak point"
204
- },
205
- "electricity_consumed_peak_point": {
206
- "name": "Electricity consumed peak point"
207
- },
208
- "electricity_consumed_off_peak_cumulative": {
209
- "name": "Electricity consumed off peak cumulative"
210
  },
211
  "electricity_consumed_peak_cumulative": {
212
  "name": "Electricity consumed peak cumulative"
213
  },
214
- "electricity_produced_off_peak_point": {
215
- "name": "Electricity produced off peak point"
216
- },
217
- "electricity_produced_peak_point": {
218
- "name": "Electricity produced peak point"
219
  },
220
- "electricity_produced_off_peak_cumulative": {
221
- "name": "Electricity produced off peak cumulative"
222
  },
223
- "electricity_produced_peak_cumulative": {
224
- "name": "Electricity produced peak cumulative"
225
  },
226
  "electricity_phase_one_consumed": {
227
  "name": "Electricity phase one consumed"
228
  },
229
- "electricity_phase_two_consumed": {
230
- "name": "Electricity phase two consumed"
231
  },
232
  "electricity_phase_three_consumed": {
233
  "name": "Electricity phase three consumed"
234
  },
235
- "electricity_phase_one_produced": {
236
- "name": "Electricity phase one produced"
 
 
 
237
  },
238
  "electricity_phase_two_produced": {
239
  "name": "Electricity phase two produced"
240
  },
241
- "electricity_phase_three_produced": {
242
- "name": "Electricity phase three produced"
243
  },
244
- "voltage_phase_one": {
245
- "name": "Voltage phase one"
246
  },
247
- "voltage_phase_two": {
248
- "name": "Voltage phase two"
249
  },
250
- "voltage_phase_three": {
251
- "name": "Voltage phase three"
252
  },
253
- "gas_consumed_interval": {
254
- "name": "Gas consumed interval"
 
 
 
 
 
 
 
 
 
 
 
 
255
  },
256
  "gas_consumed_cumulative": {
257
  "name": "Gas consumed cumulative"
258
  },
259
- "net_electricity_point": {
260
- "name": "Net electricity point"
261
  },
262
- "net_electricity_cumulative": {
263
- "name": "Net electricity cumulative"
 
 
 
 
 
 
264
  },
265
  "modulation_level": {
266
  "name": "Modulation level"
267
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  "valve_position": {
269
  "name": "Valve position"
270
  },
271
- "water_pressure": {
272
- "name": "Water pressure"
273
  },
274
- "dhw_temperature": {
275
- "name": "DHW temperature"
276
  },
277
- "domestic_hot_water_setpoint": {
278
- "name": "DHW setpoint"
279
  },
280
- "maximum_boiler_temperature": {
281
- "name": "Maximum boiler temperature setpoint"
 
 
 
282
  }
283
  },
284
  "switch": {
285
  "cooling_ena_switch": {
286
- "name": "Cooling"
287
  },
288
  "dhw_cm_switch": {
289
  "name": "DHW comfort mode"
290
  },
291
  "lock": {
292
- "name": "Lock"
293
  },
294
  "relay": {
295
  "name": "Relay"
296
  }
297
  }
298
  },
299
- "services": {
300
- "delete_notification": {
301
- "name": "Delete Plugwise notification",
302
- "description": "Deletes a Plugwise Notification"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  }
304
  }
305
  }
 
1
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  "config": {
3
+ "abort": {
4
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
5
+ "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
6
+ "not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address.",
7
+ "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
 
 
 
 
 
 
8
  },
9
  "error": {
10
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
11
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
12
  "invalid_setup": "Add your Adam instead of your Anna, see the documentation",
 
 
13
  "response_error": "Invalid XML data, or error indication received",
14
+ "unknown": "[%key:common::config_flow::error::unknown%]",
 
15
  "unsupported": "Device with unsupported firmware"
16
  },
17
+ "step": {
18
+ "reconfigure": {
19
+ "data": {
20
+ "host": "[%key:common::config_flow::data::ip%]",
21
+ "port": "[%key:common::config_flow::data::port%]"
22
+ },
23
+ "data_description": {
24
+ "host": "[%key:component::plugwise::config::step::user::data_description::host%]",
25
+ "port": "[%key:component::plugwise::config::step::user::data_description::port%]"
26
+ },
27
+ "description": "Update configuration for {title}."
28
+ },
29
+ "user": {
30
+ "data": {
31
+ "host": "[%key:common::config_flow::data::ip%]",
32
+ "password": "Smile ID",
33
+ "port": "[%key:common::config_flow::data::port%]",
34
+ "username": "Smile username"
35
+ },
36
+ "data_description": {
37
+ "host": "The hostname or IP address of your Smile. You can find it in your router or the Plugwise app.",
38
+ "password": "The Smile ID printed on the label on the back of your Adam, Smile-T, or P1.",
39
+ "port": "By default your Smile uses port 80, normally you should not have to change this.",
40
+ "username": "Default is `smile`, or `stretch` for the legacy Stretch."
41
+ },
42
+ "description": "Please enter",
43
+ "title": "Connect to the Smile"
44
+ }
45
  }
46
  },
47
  "entity": {
48
  "binary_sensor": {
 
 
 
49
  "compressor_state": {
50
  "name": "Compressor state"
51
  },
52
  "cooling_enabled": {
53
  "name": "Cooling enabled"
54
  },
55
+ "cooling_state": {
56
+ "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
57
+ },
58
  "dhw_state": {
59
  "name": "DHW state"
60
  },
 
62
  "name": "Flame state"
63
  },
64
  "heating_state": {
65
+ "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]"
66
  },
67
+ "plugwise_notification": {
68
+ "name": "Plugwise notification"
69
  },
70
  "secondary_boiler_state": {
71
  "name": "Secondary boiler state"
 
 
 
72
  }
73
  },
74
  "button": {
 
79
  "climate": {
80
  "plugwise": {
81
  "state_attributes": {
82
+ "available_schemas": {
83
+ "name": "Available schemas"
84
+ },
85
  "preset_mode": {
86
  "state": {
87
  "asleep": "Night",
88
+ "away": "[%key:common::state::not_home%]",
89
+ "home": "[%key:common::state::home%]",
90
  "no_frost": "Anti-frost",
91
  "vacation": "Vacation"
92
  }
93
+ },
94
+ "selected_schema": {
95
+ "name": "Selected schema"
96
  }
97
  }
98
  }
99
  },
100
  "number": {
 
 
 
101
  "max_dhw_temperature": {
102
  "name": "Domestic hot water setpoint"
103
  },
104
+ "maximum_boiler_temperature": {
105
+ "name": "Maximum boiler temperature setpoint"
106
+ },
107
  "temperature_offset": {
108
  "name": "Temperature offset"
109
  }
110
  },
111
  "select": {
112
+ "select_dhw_mode": {
113
  "name": "DHW mode",
114
  "state": {
115
+ "auto": "[%key:common::state::auto%]",
116
+ "boost": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::boost%]",
117
+ "comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
118
+ "off": "[%key:common::state::off%]"
119
+ }
120
+ },
121
+ "select_gateway_mode": {
122
+ "name": "Gateway mode",
123
+ "state": {
124
+ "away": "Pause",
125
+ "full": "[%key:common::state::normal%]",
126
+ "vacation": "Vacation"
127
  }
128
  },
129
+ "select_regulation_mode": {
130
  "name": "Regulation mode",
131
  "state": {
132
  "bleeding_cold": "Bleeding cold",
133
  "bleeding_hot": "Bleeding hot",
134
+ "cooling": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]",
135
+ "heating": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]",
136
+ "off": "[%key:common::state::off%]"
137
  }
138
  },
139
+ "select_schedule": {
140
+ "name": "Thermostat schedule",
141
  "state": {
142
+ "off": "[%key:common::state::off%]"
 
 
143
  }
144
  },
145
+ "select_zone_profile": {
146
+ "name": "Zone profile",
147
  "state": {
148
+ "active": "[%key:common::state::active%]",
149
+ "off": "[%key:common::state::off%]",
150
+ "passive": "Passive"
151
  }
152
  }
153
  },
154
  "sensor": {
 
 
 
155
  "cooling_setpoint": {
156
  "name": "Cooling setpoint"
157
  },
158
+ "dhw_temperature": {
159
+ "name": "DHW temperature"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  },
161
+ "domestic_hot_water_setpoint": {
162
+ "name": "DHW setpoint"
163
  },
164
  "electricity_consumed": {
165
  "name": "Electricity consumed"
166
  },
 
 
 
 
 
 
 
 
 
167
  "electricity_consumed_interval": {
168
  "name": "Electricity consumed interval"
169
  },
170
+ "electricity_consumed_off_peak_cumulative": {
171
+ "name": "Electricity consumed off-peak cumulative"
172
  },
173
  "electricity_consumed_off_peak_interval": {
174
+ "name": "Electricity consumed off-peak interval"
 
 
 
 
 
 
 
 
 
175
  },
176
  "electricity_consumed_off_peak_point": {
177
+ "name": "Electricity consumed off-peak point"
 
 
 
 
 
 
178
  },
179
  "electricity_consumed_peak_cumulative": {
180
  "name": "Electricity consumed peak cumulative"
181
  },
182
+ "electricity_consumed_peak_interval": {
183
+ "name": "Electricity consumed peak interval"
 
 
 
184
  },
185
+ "electricity_consumed_peak_point": {
186
+ "name": "Electricity consumed peak point"
187
  },
188
+ "electricity_consumed_point": {
189
+ "name": "Electricity consumed point"
190
  },
191
  "electricity_phase_one_consumed": {
192
  "name": "Electricity phase one consumed"
193
  },
194
+ "electricity_phase_one_produced": {
195
+ "name": "Electricity phase one produced"
196
  },
197
  "electricity_phase_three_consumed": {
198
  "name": "Electricity phase three consumed"
199
  },
200
+ "electricity_phase_three_produced": {
201
+ "name": "Electricity phase three produced"
202
+ },
203
+ "electricity_phase_two_consumed": {
204
+ "name": "Electricity phase two consumed"
205
  },
206
  "electricity_phase_two_produced": {
207
  "name": "Electricity phase two produced"
208
  },
209
+ "electricity_produced": {
210
+ "name": "Electricity produced"
211
  },
212
+ "electricity_produced_interval": {
213
+ "name": "Electricity produced interval"
214
  },
215
+ "electricity_produced_off_peak_cumulative": {
216
+ "name": "Electricity produced off-peak cumulative"
217
  },
218
+ "electricity_produced_off_peak_interval": {
219
+ "name": "Electricity produced off-peak interval"
220
  },
221
+ "electricity_produced_off_peak_point": {
222
+ "name": "Electricity produced off-peak point"
223
+ },
224
+ "electricity_produced_peak_cumulative": {
225
+ "name": "Electricity produced peak cumulative"
226
+ },
227
+ "electricity_produced_peak_interval": {
228
+ "name": "Electricity produced peak interval"
229
+ },
230
+ "electricity_produced_peak_point": {
231
+ "name": "Electricity produced peak point"
232
+ },
233
+ "electricity_produced_point": {
234
+ "name": "Electricity produced point"
235
  },
236
  "gas_consumed_cumulative": {
237
  "name": "Gas consumed cumulative"
238
  },
239
+ "gas_consumed_interval": {
240
+ "name": "Gas consumed interval"
241
  },
242
+ "heating_setpoint": {
243
+ "name": "Heating setpoint"
244
+ },
245
+ "intended_boiler_temperature": {
246
+ "name": "Intended boiler temperature"
247
+ },
248
+ "maximum_boiler_temperature": {
249
+ "name": "Maximum boiler temperature"
250
  },
251
  "modulation_level": {
252
  "name": "Modulation level"
253
  },
254
+ "net_electricity_cumulative": {
255
+ "name": "Net electricity cumulative"
256
+ },
257
+ "net_electricity_point": {
258
+ "name": "Net electricity point"
259
+ },
260
+ "outdoor_air_temperature": {
261
+ "name": "Outdoor air temperature"
262
+ },
263
+ "outdoor_temperature": {
264
+ "name": "Outdoor temperature"
265
+ },
266
+ "return_temperature": {
267
+ "name": "Return temperature"
268
+ },
269
+ "setpoint": {
270
+ "name": "Setpoint"
271
+ },
272
+ "temperature_difference": {
273
+ "name": "Temperature difference"
274
+ },
275
  "valve_position": {
276
  "name": "Valve position"
277
  },
278
+ "voltage_phase_one": {
279
+ "name": "Voltage phase one"
280
  },
281
+ "voltage_phase_three": {
282
+ "name": "Voltage phase three"
283
  },
284
+ "voltage_phase_two": {
285
+ "name": "Voltage phase two"
286
  },
287
+ "water_pressure": {
288
+ "name": "Water pressure"
289
+ },
290
+ "water_temperature": {
291
+ "name": "Water temperature"
292
  }
293
  },
294
  "switch": {
295
  "cooling_ena_switch": {
296
+ "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
297
  },
298
  "dhw_cm_switch": {
299
  "name": "DHW comfort mode"
300
  },
301
  "lock": {
302
+ "name": "[%key:component::lock::title%]"
303
  },
304
  "relay": {
305
  "name": "Relay"
306
  }
307
  }
308
  },
309
+ "exceptions": {
310
+ "authentication_failed": {
311
+ "message": "[%key:common::config_flow::error::invalid_auth%]"
312
+ },
313
+ "data_incomplete_or_missing": {
314
+ "message": "Data incomplete or missing."
315
+ },
316
+ "error_communicating_with_api": {
317
+ "message": "Error communicating with API: {error}."
318
+ },
319
+ "failed_to_connect": {
320
+ "message": "[%key:common::config_flow::error::cannot_connect%]"
321
+ },
322
+ "invalid_setup": {
323
+ "message": "Add your Adam instead of your Anna, see the documentation"
324
+ },
325
+ "response_error": {
326
+ "message": "[%key:component::plugwise::config::error::response_error%]"
327
+ },
328
+ "set_schedule_first": {
329
+ "message": "Failed setting HVACMode, set a schedule first."
330
+ },
331
+ "unsupported_firmware": {
332
+ "message": "[%key:component::plugwise::config::error::unsupported%]"
333
  }
334
  }
335
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py RENAMED
@@ -1,9 +1,7 @@
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
 
@@ -14,25 +12,13 @@
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 .const import (
21
- COOLING_ENA_SWITCH,
22
- DHW_CM_SWITCH,
23
- LOCK,
24
- LOGGER, # pw-beta
25
- MEMBERS,
26
- RELAY,
27
- SWITCHES,
28
- )
29
 
30
- # Upstream consts
31
- from .coordinator import PlugwiseDataUpdateCoordinator
32
  from .entity import PlugwiseEntity
33
  from .util import plugwise_command
34
 
35
- PARALLEL_UPDATES = 0 # Upstream
36
 
37
 
38
  @dataclass(frozen=True)
@@ -42,29 +28,25 @@
42
  key: SwitchType
43
 
44
 
45
- # Upstream consts
46
- PLUGWISE_SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
47
  PlugwiseSwitchEntityDescription(
48
- key=DHW_CM_SWITCH,
49
- translation_key=DHW_CM_SWITCH,
50
- device_class=SwitchDeviceClass.SWITCH,
51
  entity_category=EntityCategory.CONFIG,
52
  ),
53
  PlugwiseSwitchEntityDescription(
54
- key=LOCK,
55
- translation_key=LOCK,
56
- device_class=SwitchDeviceClass.SWITCH,
57
  entity_category=EntityCategory.CONFIG,
58
  ),
59
  PlugwiseSwitchEntityDescription(
60
- key=RELAY,
61
- translation_key=RELAY,
62
  device_class=SwitchDeviceClass.SWITCH,
63
  ),
64
  PlugwiseSwitchEntityDescription(
65
- key=COOLING_ENA_SWITCH,
66
- translation_key=COOLING_ENA_SWITCH,
67
- device_class=SwitchDeviceClass.SWITCH,
68
  entity_category=EntityCategory.CONFIG,
69
  ),
70
  )
@@ -73,9 +55,9 @@
73
  async def async_setup_entry(
74
  hass: HomeAssistant,
75
  entry: PlugwiseConfigEntry,
76
- async_add_entities: AddEntitiesCallback,
77
  ) -> None:
78
- """Set up Plugwise switches from a config entry."""
79
  coordinator = entry.runtime_data
80
 
81
  @callback
@@ -84,28 +66,13 @@
84
  if not coordinator.new_devices:
85
  return
86
 
87
- # Upstream consts
88
- # async_add_entities(
89
- # PlugwiseSwitchEntity(coordinator, device_id, description)
90
- # for device_id in coordinator.new_devices
91
- # if (switches := coordinator.data.devices[device_id].get(SWITCHES))
92
- # for description in PLUGWISE_SWITCHES
93
- # if description.key in switches
94
- # )
95
- # pw-beta alternative for debugging
96
- entities: list[PlugwiseSwitchEntity] = []
97
- for device_id in coordinator.new_devices:
98
- device = coordinator.data.devices[device_id]
99
- if not (switches := device.get(SWITCHES)):
100
- continue
101
- for description in PLUGWISE_SWITCHES:
102
- if description.key not in switches:
103
- continue
104
- entities.append(PlugwiseSwitchEntity(coordinator, device_id, description))
105
- LOGGER.debug(
106
- "Add %s %s switch", device["name"], description.translation_key
107
- )
108
- async_add_entities(entities)
109
 
110
  _add_entities()
111
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -124,30 +91,33 @@
124
  ) -> None:
125
  """Set up the Plugwise API."""
126
  super().__init__(coordinator, device_id)
127
- self.entity_description = description
128
  self._attr_unique_id = f"{device_id}-{description.key}"
 
129
 
130
  @property
 
131
  def is_on(self) -> bool:
132
  """Return True if entity is on."""
133
- return self.device[SWITCHES][self.entity_description.key] # Upstream const
134
 
135
  @plugwise_command
 
136
  async def async_turn_on(self, **kwargs: Any) -> None:
137
  """Turn the device on."""
138
  await self.coordinator.api.set_switch_state(
139
  self._dev_id,
140
- self.device.get(MEMBERS),
141
  self.entity_description.key,
142
  "on",
143
- ) # Upstream const
144
 
145
  @plugwise_command
 
146
  async def async_turn_off(self, **kwargs: Any) -> None:
147
  """Turn the device off."""
148
  await self.coordinator.api.set_switch_state(
149
  self._dev_id,
150
- self.device.get(MEMBERS),
151
  self.entity_description.key,
152
  "off",
153
- ) # Upstream const
 
1
  """Plugwise Switch component for HomeAssistant."""
2
 
 
 
3
  from dataclasses import dataclass
4
+ from typing import Any, override
5
 
6
  from plugwise.constants import SwitchType
7
 
 
12
  )
13
  from homeassistant.const import EntityCategory
14
  from homeassistant.core import HomeAssistant, callback
15
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
18
  from .entity import PlugwiseEntity
19
  from .util import plugwise_command
20
 
21
+ PARALLEL_UPDATES = 0
22
 
23
 
24
  @dataclass(frozen=True)
 
28
  key: SwitchType
29
 
30
 
31
+ SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
 
32
  PlugwiseSwitchEntityDescription(
33
+ key="dhw_cm_switch",
34
+ translation_key="dhw_cm_switch",
 
35
  entity_category=EntityCategory.CONFIG,
36
  ),
37
  PlugwiseSwitchEntityDescription(
38
+ key="lock",
39
+ translation_key="lock",
 
40
  entity_category=EntityCategory.CONFIG,
41
  ),
42
  PlugwiseSwitchEntityDescription(
43
+ key="relay",
44
+ translation_key="relay",
45
  device_class=SwitchDeviceClass.SWITCH,
46
  ),
47
  PlugwiseSwitchEntityDescription(
48
+ key="cooling_ena_switch",
49
+ translation_key="cooling_ena_switch",
 
50
  entity_category=EntityCategory.CONFIG,
51
  ),
52
  )
 
55
  async def async_setup_entry(
56
  hass: HomeAssistant,
57
  entry: PlugwiseConfigEntry,
58
+ async_add_entities: AddConfigEntryEntitiesCallback,
59
  ) -> None:
60
+ """Set up the Smile switches from a config entry."""
61
  coordinator = entry.runtime_data
62
 
63
  @callback
 
66
  if not coordinator.new_devices:
67
  return
68
 
69
+ async_add_entities(
70
+ PlugwiseSwitchEntity(coordinator, device_id, description)
71
+ for device_id in coordinator.new_devices
72
+ if (switches := coordinator.data[device_id].get("switches"))
73
+ for description in SWITCHES
74
+ if description.key in switches
75
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  _add_entities()
78
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
91
  ) -> None:
92
  """Set up the Plugwise API."""
93
  super().__init__(coordinator, device_id)
 
94
  self._attr_unique_id = f"{device_id}-{description.key}"
95
+ self.entity_description = description
96
 
97
  @property
98
+ @override
99
  def is_on(self) -> bool:
100
  """Return True if entity is on."""
101
+ return self.device["switches"][self.entity_description.key]
102
 
103
  @plugwise_command
104
+ @override
105
  async def async_turn_on(self, **kwargs: Any) -> None:
106
  """Turn the device on."""
107
  await self.coordinator.api.set_switch_state(
108
  self._dev_id,
109
+ self.device.get("members"),
110
  self.entity_description.key,
111
  "on",
112
+ )
113
 
114
  @plugwise_command
115
+ @override
116
  async def async_turn_off(self, **kwargs: Any) -> None:
117
  """Turn the device off."""
118
  await self.coordinator.api.set_switch_state(
119
  self._dev_id,
120
+ self.device.get("members"),
121
  self.entity_description.key,
122
  "off",
123
+ )
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py RENAMED
@@ -1,7 +1,5 @@
1
  """Utilities for Plugwise."""
2
 
3
- from __future__ import annotations
4
-
5
  from collections.abc import Awaitable, Callable, Coroutine
6
  from typing import Any, Concatenate
7
 
@@ -9,13 +7,9 @@
9
 
10
  from homeassistant.exceptions import HomeAssistantError
11
 
 
12
  from .entity import PlugwiseEntity
13
 
14
- # For reference:
15
- # _PlugwiseEntityT = TypeVar("_PlugwiseEntityT", bound=PlugwiseEntity)
16
- # _R = TypeVar("_R")
17
- # _P = ParamSpec("_P")
18
-
19
 
20
  def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
21
  func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],
@@ -31,10 +25,14 @@
31
  ) -> _R:
32
  try:
33
  return await func(self, *args, **kwargs)
34
- except PlugwiseException as error:
35
  raise HomeAssistantError(
36
- f"Error communicating with API: {error}"
37
- ) from error
 
 
 
 
38
  finally:
39
  await self.coordinator.async_request_refresh()
40
 
 
1
  """Utilities for Plugwise."""
2
 
 
 
3
  from collections.abc import Awaitable, Callable, Coroutine
4
  from typing import Any, Concatenate
5
 
 
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]],
 
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