Diff to HTML by rtfpessoa

Files changed (16) hide show
  1. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/__init__.py +84 -17
  2. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/binary_sensor.py +94 -38
  3. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/button.py +18 -7
  4. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/climate.py +135 -69
  5. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/config_flow.py +138 -30
  6. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/const.py +156 -25
  7. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/coordinator.py +44 -18
  8. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/entity.py +29 -13
  9. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/icons.json +3 -0
  10. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/manifest.json +3 -2
  11. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/number.py +53 -22
  12. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/select.py +61 -22
  13. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/sensor.py +171 -100
  14. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/strings.json +69 -62
  15. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/switch.py +53 -23
  16. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/util.py +7 -0
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/__init__.py RENAMED
@@ -4,24 +4,47 @@
4
 
5
  from typing import Any
6
 
 
 
 
7
  from homeassistant.config_entries import ConfigEntry
8
- from homeassistant.const import Platform
9
- from homeassistant.core import HomeAssistant, callback
 
 
 
 
10
  from homeassistant.helpers import device_registry as dr, entity_registry as er
11
 
12
- from .const import DOMAIN, LOGGER, PLATFORMS
 
 
 
 
 
 
13
  from .coordinator import PlugwiseDataUpdateCoordinator
14
 
15
  type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
16
 
17
 
18
  async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
19
- """Set up Plugwise components from a config entry."""
20
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
21
 
22
- coordinator = PlugwiseDataUpdateCoordinator(hass)
 
 
 
 
 
 
 
 
 
23
  await coordinator.async_config_entry_first_refresh()
24
- migrate_sensor_entities(hass, coordinator)
 
25
 
26
  entry.runtime_data = coordinator
27
 
@@ -36,16 +59,44 @@
36
  sw_version=str(coordinator.api.smile_version),
37
  ) # required for adding the entity-less P1 Gateway
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
40
 
 
 
 
 
 
 
 
41
  return True
42
 
 
 
 
 
 
43
 
44
  async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
45
- """Unload the Plugwise components."""
46
  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
47
 
48
-
49
  @callback
50
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
51
  """Migrate Plugwise entity entries.
@@ -72,15 +123,14 @@
72
  # No migration needed
73
  return None
74
 
75
-
76
- def migrate_sensor_entities(
77
  hass: HomeAssistant,
78
  coordinator: PlugwiseDataUpdateCoordinator,
79
  ) -> None:
80
  """Migrate Sensors if needed."""
81
  ent_reg = er.async_get(hass)
82
 
83
- # Migrating opentherm_outdoor_temperature
84
  # to opentherm_outdoor_air_temperature sensor
85
  for device_id, device in coordinator.data.devices.items():
86
  if device["dev_class"] != "heater_central":
@@ -91,10 +141,27 @@
91
  Platform.SENSOR, DOMAIN, old_unique_id
92
  ):
93
  new_unique_id = f"{device_id}-outdoor_air_temperature"
94
- LOGGER.debug(
95
- "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
96
- entity_id,
97
- old_unique_id,
98
- new_unique_id,
99
- )
100
  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
 
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.
 
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":
 
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
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/binary_sensor.py RENAMED
@@ -8,6 +8,9 @@
8
 
9
  from plugwise.constants import BinarySensorType
10
 
 
 
 
11
  from homeassistant.components.binary_sensor import (
12
  BinarySensorDeviceClass,
13
  BinarySensorEntity,
@@ -18,11 +21,27 @@
18
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
19
 
20
  from . import PlugwiseConfigEntry
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  from .coordinator import PlugwiseDataUpdateCoordinator
22
  from .entity import PlugwiseEntity
23
 
24
- SEVERITIES = ["other", "info", "warning", "error"]
25
-
26
  # Coordinator is used to centralize the data updates
27
  PARALLEL_UPDATES = 0
28
 
@@ -34,50 +53,51 @@
34
  key: BinarySensorType
35
 
36
 
37
- BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
 
38
  PlugwiseBinarySensorEntityDescription(
39
- key="low_battery",
40
  device_class=BinarySensorDeviceClass.BATTERY,
41
  entity_category=EntityCategory.DIAGNOSTIC,
42
  ),
43
  PlugwiseBinarySensorEntityDescription(
44
- key="compressor_state",
45
- translation_key="compressor_state",
46
  entity_category=EntityCategory.DIAGNOSTIC,
47
  ),
48
  PlugwiseBinarySensorEntityDescription(
49
- key="cooling_enabled",
50
- translation_key="cooling_enabled",
51
  entity_category=EntityCategory.DIAGNOSTIC,
52
  ),
53
  PlugwiseBinarySensorEntityDescription(
54
- key="dhw_state",
55
- translation_key="dhw_state",
56
  entity_category=EntityCategory.DIAGNOSTIC,
57
  ),
58
  PlugwiseBinarySensorEntityDescription(
59
- key="flame_state",
60
- translation_key="flame_state",
61
  entity_category=EntityCategory.DIAGNOSTIC,
62
  ),
63
  PlugwiseBinarySensorEntityDescription(
64
- key="heating_state",
65
- translation_key="heating_state",
66
  entity_category=EntityCategory.DIAGNOSTIC,
67
  ),
68
  PlugwiseBinarySensorEntityDescription(
69
- key="cooling_state",
70
- translation_key="cooling_state",
71
  entity_category=EntityCategory.DIAGNOSTIC,
72
  ),
73
  PlugwiseBinarySensorEntityDescription(
74
- key="secondary_boiler_state",
75
- translation_key="secondary_boiler_state",
76
  entity_category=EntityCategory.DIAGNOSTIC,
77
  ),
78
  PlugwiseBinarySensorEntityDescription(
79
- key="plugwise_notification",
80
- translation_key="plugwise_notification",
81
  entity_category=EntityCategory.DIAGNOSTIC,
82
  ),
83
  )
@@ -88,7 +108,7 @@
88
  entry: PlugwiseConfigEntry,
89
  async_add_entities: AddEntitiesCallback,
90
  ) -> None:
91
- """Set up the Smile binary_sensors from a config entry."""
92
  coordinator = entry.runtime_data
93
 
94
  @callback
@@ -97,24 +117,40 @@
97
  if not coordinator.new_devices:
98
  return
99
 
100
- async_add_entities(
101
- PlugwiseBinarySensorEntity(coordinator, device_id, description)
102
- for device_id in coordinator.new_devices
103
- if (
104
- binary_sensors := coordinator.data.devices[device_id].get(
105
- "binary_sensors"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  )
107
- )
108
- for description in BINARY_SENSORS
109
- if description.key in binary_sensors
110
- )
111
 
112
  _add_entities()
113
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
114
 
115
 
116
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
117
- """Represent Smile Binary Sensors."""
118
 
119
  entity_description: PlugwiseBinarySensorEntityDescription
120
 
@@ -128,25 +164,45 @@
128
  super().__init__(coordinator, device_id)
129
  self.entity_description = description
130
  self._attr_unique_id = f"{device_id}-{description.key}"
 
131
 
132
  @property
133
  def is_on(self) -> bool:
134
  """Return true if the binary sensor is on."""
135
- return self.device["binary_sensors"][self.entity_description.key]
 
 
 
 
 
 
 
136
 
137
  @property
138
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
139
  """Return entity specific state attributes."""
140
- if self.entity_description.key != "plugwise_notification":
141
  return None
142
 
143
- attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES}
144
- if notify := self.coordinator.data.gateway["notifications"]:
145
- for details in notify.values():
 
 
 
146
  for msg_type, msg in details.items():
147
  msg_type = msg_type.lower()
148
  if msg_type not in SEVERITIES:
149
- msg_type = "other"
 
 
 
 
 
150
  attrs[f"{msg_type}_msg"].append(msg)
151
 
 
 
 
 
152
  return attrs
 
8
 
9
  from plugwise.constants import BinarySensorType
10
 
11
+ from homeassistant.components import (
12
+ persistent_notification, # pw-beta Plugwise notifications
13
+ )
14
  from homeassistant.components.binary_sensor import (
15
  BinarySensorDeviceClass,
16
  BinarySensorEntity,
 
21
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
22
 
23
  from . import PlugwiseConfigEntry
24
+ from .const import (
25
+ BATTERY_STATE,
26
+ BINARY_SENSORS,
27
+ COMPRESSOR_STATE,
28
+ COOLING_ENABLED,
29
+ COOLING_STATE,
30
+ DHW_STATE,
31
+ DOMAIN,
32
+ FLAME_STATE,
33
+ HEATING_STATE,
34
+ LOGGER, # pw-beta
35
+ NOTIFICATIONS,
36
+ PLUGWISE_NOTIFICATION,
37
+ SECONDARY_BOILER_STATE,
38
+ SEVERITIES,
39
+ )
40
+
41
+ # Upstream
42
  from .coordinator import PlugwiseDataUpdateCoordinator
43
  from .entity import PlugwiseEntity
44
 
 
 
45
  # Coordinator is used to centralize the data updates
46
  PARALLEL_UPDATES = 0
47
 
 
53
  key: BinarySensorType
54
 
55
 
56
+ # Upstream PLUGWISE_BINARY_SENSORS
57
+ PLUGWISE_BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
58
  PlugwiseBinarySensorEntityDescription(
59
+ key=BATTERY_STATE,
60
  device_class=BinarySensorDeviceClass.BATTERY,
61
  entity_category=EntityCategory.DIAGNOSTIC,
62
  ),
63
  PlugwiseBinarySensorEntityDescription(
64
+ key=COMPRESSOR_STATE,
65
+ translation_key=COMPRESSOR_STATE,
66
  entity_category=EntityCategory.DIAGNOSTIC,
67
  ),
68
  PlugwiseBinarySensorEntityDescription(
69
+ key=COOLING_ENABLED,
70
+ translation_key=COOLING_ENABLED,
71
  entity_category=EntityCategory.DIAGNOSTIC,
72
  ),
73
  PlugwiseBinarySensorEntityDescription(
74
+ key=DHW_STATE,
75
+ translation_key=DHW_STATE,
76
  entity_category=EntityCategory.DIAGNOSTIC,
77
  ),
78
  PlugwiseBinarySensorEntityDescription(
79
+ key=FLAME_STATE,
80
+ translation_key=FLAME_STATE,
81
  entity_category=EntityCategory.DIAGNOSTIC,
82
  ),
83
  PlugwiseBinarySensorEntityDescription(
84
+ key=HEATING_STATE,
85
+ translation_key=HEATING_STATE,
86
  entity_category=EntityCategory.DIAGNOSTIC,
87
  ),
88
  PlugwiseBinarySensorEntityDescription(
89
+ key=COOLING_STATE,
90
+ translation_key=COOLING_STATE,
91
  entity_category=EntityCategory.DIAGNOSTIC,
92
  ),
93
  PlugwiseBinarySensorEntityDescription(
94
+ key=SECONDARY_BOILER_STATE,
95
+ translation_key=SECONDARY_BOILER_STATE,
96
  entity_category=EntityCategory.DIAGNOSTIC,
97
  ),
98
  PlugwiseBinarySensorEntityDescription(
99
+ key=PLUGWISE_NOTIFICATION,
100
+ translation_key=PLUGWISE_NOTIFICATION,
101
  entity_category=EntityCategory.DIAGNOSTIC,
102
  ),
103
  )
 
108
  entry: PlugwiseConfigEntry,
109
  async_add_entities: AddEntitiesCallback,
110
  ) -> None:
111
+ """Set up Plugwise binary_sensors from a config entry."""
112
  coordinator = entry.runtime_data
113
 
114
  @callback
 
117
  if not coordinator.new_devices:
118
  return
119
 
120
+ # Upstream consts to HA
121
+ # async_add_entities(
122
+ # PlugwiseBinarySensorEntity(coordinator, device_id, description)
123
+ # for device_id in coordinator.new_devices
124
+ # if (
125
+ # binary_sensors := coordinator.data.devices[device_id].get(
126
+ # BINARY_SENSORS
127
+ # )
128
+ # )
129
+ # for description in PLUGWISE_BINARY_SENSORS
130
+ # if description.key in binary_sensors
131
+ # )
132
+
133
+ # pw-beta alternative for debugging
134
+ entities: list[PlugwiseBinarySensorEntity] = []
135
+ for device_id in coordinator.new_devices:
136
+ device = coordinator.data.devices[device_id]
137
+ if not (binary_sensors := device.get(BINARY_SENSORS)):
138
+ continue
139
+ for description in PLUGWISE_BINARY_SENSORS:
140
+ if description.key not in binary_sensors:
141
+ continue
142
+ entities.append(PlugwiseBinarySensorEntity(coordinator, device_id, description))
143
+ LOGGER.debug(
144
+ "Add %s %s binary sensor", device["name"], description.translation_key
145
  )
146
+ async_add_entities(entities)
 
 
 
147
 
148
  _add_entities()
149
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
150
 
151
 
152
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
153
+ """Set up Plugwise binary_sensors from a config entry."""
154
 
155
  entity_description: PlugwiseBinarySensorEntityDescription
156
 
 
164
  super().__init__(coordinator, device_id)
165
  self.entity_description = description
166
  self._attr_unique_id = f"{device_id}-{description.key}"
167
+ self._notification: dict[str, str] = {} # pw-beta
168
 
169
  @property
170
  def is_on(self) -> bool:
171
  """Return true if the binary sensor is on."""
172
+ # pw-beta: show Plugwise notifications as HA persistent notifications
173
+ if self._notification:
174
+ for notify_id, message in self._notification.items():
175
+ persistent_notification.async_create(
176
+ self.hass, message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
177
+ )
178
+
179
+ return self.device[BINARY_SENSORS][self.entity_description.key]
180
 
181
  @property
182
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
183
  """Return entity specific state attributes."""
184
+ if self.entity_description.key != PLUGWISE_NOTIFICATION: # Upstream const
185
  return None
186
 
187
+ # pw-beta adjustment with attrs is to only represent severities *with* content
188
+ # not all severities including those without content as empty lists
189
+ attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
190
+ self._notification = {} # pw-beta
191
+ if notify := self.coordinator.data.gateway[NOTIFICATIONS]:
192
+ for notify_id, details in notify.items(): # pw-beta uses notify_id
193
  for msg_type, msg in details.items():
194
  msg_type = msg_type.lower()
195
  if msg_type not in SEVERITIES:
196
+ msg_type = "other" # pragma: no cover
197
+
198
+ if (
199
+ f"{msg_type}_msg" not in attrs
200
+ ): # pw-beta Re-evaluate against Core
201
+ attrs[f"{msg_type}_msg"] = []
202
  attrs[f"{msg_type}_msg"].append(msg)
203
 
204
+ self._notification[
205
+ notify_id
206
+ ] = f"{msg_type.title()}: {msg}" # pw-beta
207
+
208
  return attrs
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/button.py RENAMED
@@ -8,7 +8,11 @@
8
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
9
 
10
  from . import PlugwiseConfigEntry
11
- from .const import GATEWAY_ID, REBOOT
 
 
 
 
12
  from .coordinator import PlugwiseDataUpdateCoordinator
13
  from .entity import PlugwiseEntity
14
  from .util import plugwise_command
@@ -21,15 +25,22 @@
21
  entry: PlugwiseConfigEntry,
22
  async_add_entities: AddEntitiesCallback,
23
  ) -> None:
24
- """Set up the Plugwise buttons from a ConfigEntry."""
25
  coordinator = entry.runtime_data
26
 
27
  gateway = coordinator.data.gateway
28
- async_add_entities(
29
- PlugwiseButtonEntity(coordinator, device_id)
30
- for device_id in coordinator.data.devices
31
- if device_id == gateway[GATEWAY_ID] and REBOOT in gateway
32
- )
 
 
 
 
 
 
 
33
 
34
 
35
  class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
 
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
 
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):
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/climate.py RENAMED
@@ -8,18 +8,50 @@
8
  ATTR_HVAC_MODE,
9
  ATTR_TARGET_TEMP_HIGH,
10
  ATTR_TARGET_TEMP_LOW,
 
 
11
  ClimateEntity,
12
  ClimateEntityFeature,
13
  HVACAction,
14
  HVACMode,
15
  )
16
- from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
 
 
 
 
 
 
17
  from homeassistant.core import HomeAssistant, callback
18
- from homeassistant.exceptions import ServiceValidationError
19
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
20
 
21
  from . import PlugwiseConfigEntry
22
- from .const import DOMAIN, MASTER_THERMOSTATS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  from .coordinator import PlugwiseDataUpdateCoordinator
24
  from .entity import PlugwiseEntity
25
  from .util import plugwise_command
@@ -32,28 +64,39 @@
32
  entry: PlugwiseConfigEntry,
33
  async_add_entities: AddEntitiesCallback,
34
  ) -> None:
35
- """Set up the Smile Thermostats from a config entry."""
36
  coordinator = entry.runtime_data
 
 
 
37
 
38
  @callback
39
  def _add_entities() -> None:
40
- """Add Entities."""
41
  if not coordinator.new_devices:
42
  return
43
 
44
- if coordinator.data.gateway["smile_name"] == "Adam":
45
- async_add_entities(
46
- PlugwiseClimateEntity(coordinator, device_id)
47
- for device_id in coordinator.new_devices
48
- if coordinator.data.devices[device_id]["dev_class"] == "climate"
49
- )
50
- else:
51
- async_add_entities(
52
- PlugwiseClimateEntity(coordinator, device_id)
53
- for device_id in coordinator.new_devices
54
- if coordinator.data.devices[device_id]["dev_class"]
55
- in MASTER_THERMOSTATS
56
- )
 
 
 
 
 
 
 
 
57
 
58
  _add_entities()
59
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -62,33 +105,48 @@
62
  class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
63
  """Representation of a Plugwise thermostat."""
64
 
 
65
  _attr_name = None
66
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
67
  _attr_translation_key = DOMAIN
 
68
 
69
- _previous_mode: str = "heating"
 
70
 
71
  def __init__(
72
  self,
73
  coordinator: PlugwiseDataUpdateCoordinator,
74
  device_id: str,
 
75
  ) -> None:
76
  """Set up the Plugwise API."""
77
  super().__init__(coordinator, device_id)
78
- self._attr_unique_id = f"{device_id}-climate"
79
 
80
  self._devices = coordinator.data.devices
81
  self._gateway = coordinator.data.gateway
82
- gateway_id: str = self._gateway["gateway_id"]
83
  self._gateway_data = self._devices[gateway_id]
 
84
 
85
  self._location = device_id
86
- if (location := self.device.get("location")) is not None:
87
  self._location = location
88
 
 
 
 
 
 
 
 
 
89
  # Determine supported features
90
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
91
- if self._gateway["cooling_present"] and self._gateway["smile_name"] != "Adam":
 
 
 
92
  self._attr_supported_features = (
93
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
94
  )
@@ -96,16 +154,9 @@
96
  self._attr_supported_features |= (
97
  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
98
  )
99
- if presets := self.device.get("preset_modes"):
100
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
101
- self._attr_preset_modes = presets
102
-
103
- self._attr_min_temp = self.device["thermostat"]["lower_bound"]
104
- self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
105
- # Ensure we don't drop below 0.1
106
- self._attr_target_temperature_step = max(
107
- self.device["thermostat"]["resolution"], 0.1
108
- )
109
 
110
  def _previous_action_mode(self, coordinator: PlugwiseDataUpdateCoordinator) -> None:
111
  """Return the previous action-mode when the regulation-mode is not heating or cooling.
@@ -114,17 +165,17 @@
114
  """
115
  # When no cooling available, _previous_mode is always heating
116
  if (
117
- "regulation_modes" in self._gateway_data
118
- and "cooling" in self._gateway_data["regulation_modes"]
119
  ):
120
- mode = self._gateway_data["select_regulation_mode"]
121
- if mode in ("cooling", "heating"):
122
  self._previous_mode = mode
123
 
124
  @property
125
  def current_temperature(self) -> float:
126
  """Return the current temperature."""
127
- return self.device["sensors"]["temperature"]
128
 
129
  @property
130
  def target_temperature(self) -> float:
@@ -133,7 +184,7 @@
133
  Connected to the HVACMode combination of AUTO-HEAT.
134
  """
135
 
136
- return self.device["thermostat"]["setpoint"]
137
 
138
  @property
139
  def target_temperature_high(self) -> float:
@@ -141,7 +192,7 @@
141
 
142
  Connected to the HVACMode combination of AUTO-HEAT_COOL.
143
  """
144
- return self.device["thermostat"]["setpoint_high"]
145
 
146
  @property
147
  def target_temperature_low(self) -> float:
@@ -149,32 +200,39 @@
149
 
150
  Connected to the HVACMode combination AUTO-HEAT_COOL.
151
  """
152
- return self.device["thermostat"]["setpoint_low"]
153
 
154
  @property
155
  def hvac_mode(self) -> HVACMode:
156
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
157
  if (
158
- mode := self.device.get("climate_mode")
159
- ) is None or mode not in self.hvac_modes:
160
- return HVACMode.HEAT
 
 
 
 
161
  return HVACMode(mode)
162
 
163
  @property
164
  def hvac_modes(self) -> list[HVACMode]:
165
  """Return a list of available HVACModes."""
166
  hvac_modes: list[HVACMode] = []
167
- if "regulation_modes" in self._gateway_data:
 
 
 
168
  hvac_modes.append(HVACMode.OFF)
169
 
170
- if "available_schedules" in self.device:
171
  hvac_modes.append(HVACMode.AUTO)
172
 
173
- if self._gateway["cooling_present"]:
174
- if "regulation_modes" in self._gateway_data:
175
- if self._gateway_data["select_regulation_mode"] == "cooling":
176
  hvac_modes.append(HVACMode.COOL)
177
- if self._gateway_data["select_regulation_mode"] == "heating":
178
  hvac_modes.append(HVACMode.HEAT)
179
  else:
180
  hvac_modes.append(HVACMode.HEAT_COOL)
@@ -184,11 +242,12 @@
184
  return hvac_modes
185
 
186
  @property
187
- def hvac_action(self) -> HVACAction:
188
  """Return the current running hvac operation if supported."""
189
  # Keep track of the previous action-mode
190
  self._previous_action_mode(self.coordinator)
191
- if (action := self.device.get("control_state")) is not None:
 
192
  return HVACAction(action)
193
 
194
  return HVACAction.IDLE
@@ -196,18 +255,20 @@
196
  @property
197
  def preset_mode(self) -> str | None:
198
  """Return the current preset mode."""
199
- return self.device.get("active_preset")
200
 
201
  @plugwise_command
202
  async def async_set_temperature(self, **kwargs: Any) -> None:
203
  """Set new target temperature."""
204
  data: dict[str, Any] = {}
205
  if ATTR_TEMPERATURE in kwargs:
206
- data["setpoint"] = kwargs.get(ATTR_TEMPERATURE)
207
  if ATTR_TARGET_TEMP_HIGH in kwargs:
208
- data["setpoint_high"] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
209
  if ATTR_TARGET_TEMP_LOW in kwargs:
210
- data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
 
 
211
 
212
  if mode := kwargs.get(ATTR_HVAC_MODE):
213
  await self.async_set_hvac_mode(mode)
@@ -218,28 +279,33 @@
218
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
219
  """Set the hvac mode."""
220
  if hvac_mode not in self.hvac_modes:
221
- hvac_modes = ", ".join(self.hvac_modes)
222
- raise ServiceValidationError(
223
- translation_domain=DOMAIN,
224
- translation_key="unsupported_hvac_mode_requested",
225
- translation_placeholders={
226
- "hvac_mode": hvac_mode,
227
- "hvac_modes": hvac_modes,
228
- },
229
- )
230
 
231
  if hvac_mode == self.hvac_mode:
232
  return
233
 
234
- if hvac_mode == HVACMode.OFF:
235
- await self.coordinator.api.set_regulation_mode(hvac_mode)
236
- else:
237
  await self.coordinator.api.set_schedule_state(
238
  self._location,
239
- "on" if hvac_mode == HVACMode.AUTO else "off",
240
  )
241
- if self.hvac_mode == HVACMode.OFF:
 
 
 
 
 
 
242
  await self.coordinator.api.set_regulation_mode(self._previous_mode)
 
 
 
 
 
 
 
 
 
243
 
244
  @plugwise_command
245
  async def async_set_preset_mode(self, preset_mode: str) -> None:
 
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
 
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))
 
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
  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.
 
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:
 
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:
 
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:
 
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)
 
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
 
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)
 
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:
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/config_flow.py RENAMED
@@ -2,6 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import logging
6
  from typing import Any, Self
7
 
@@ -16,7 +17,16 @@
16
  )
17
  import voluptuous as vol
18
 
19
- from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
 
 
 
 
 
 
 
 
 
20
  from homeassistant.const import (
21
  ATTR_CONFIGURATION_URL,
22
  CONF_BASE,
@@ -24,26 +34,46 @@
24
  CONF_NAME,
25
  CONF_PASSWORD,
26
  CONF_PORT,
 
27
  CONF_USERNAME,
28
  )
29
- from homeassistant.core import HomeAssistant
 
 
 
30
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
31
- from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
32
 
33
  from .const import (
 
 
 
34
  DEFAULT_PORT,
 
35
  DEFAULT_USERNAME,
36
  DOMAIN,
37
  FLOW_SMILE,
38
  FLOW_STRETCH,
 
39
  SMILE,
 
 
40
  STRETCH,
41
  STRETCH_USERNAME,
 
 
 
42
  ZEROCONF_MAP,
43
  )
44
 
 
 
 
 
 
45
  _LOGGER = logging.getLogger(__name__)
46
 
 
 
47
  SMILE_RECONF_SCHEMA = vol.Schema(
48
  {
49
  vol.Required(CONF_HOST): str,
@@ -51,14 +81,13 @@
51
  )
52
 
53
 
54
- def smile_user_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
55
  """Generate base schema for gateways."""
56
- schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
57
-
58
- if not discovery_info:
59
- schema = schema.extend(
60
  {
61
  vol.Required(CONF_HOST): str,
 
62
  # Port under investigation for removal (hence not added in #132878)
63
  vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
64
  vol.Required(CONF_USERNAME, default=SMILE): vol.In(
@@ -67,7 +96,19 @@
67
  }
68
  )
69
 
70
- return schema
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
@@ -105,7 +146,7 @@
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
  )
@@ -117,6 +158,7 @@
117
  """Handle a config flow for Plugwise Smile."""
118
 
119
  VERSION = 1
 
120
 
121
  discovery_info: ZeroconfServiceInfo | None = None
122
  product: str = "Unknown Smile"
@@ -128,17 +170,21 @@
128
  """Prepare configuration for a discovered Plugwise Smile."""
129
  self.discovery_info = discovery_info
130
  _properties = discovery_info.properties
131
-
 
132
  unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
 
 
 
133
  if config_entry := await self.async_set_unique_id(unique_id):
134
  try:
135
  await validate_input(
136
  self.hass,
137
  {
138
  CONF_HOST: discovery_info.host,
 
139
  CONF_PORT: discovery_info.port,
140
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
141
- CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
142
  },
143
  )
144
  except Exception: # noqa: BLE001
@@ -151,16 +197,10 @@
151
  }
152
  )
153
 
154
- if DEFAULT_USERNAME not in unique_id:
155
- self._username = STRETCH_USERNAME
156
- self.product = _product = _properties.get("product", "Unknown Smile")
157
- _version = _properties.get("version", "n/a")
158
- _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
159
-
160
  # This is an Anna, but we already have config entries.
161
  # Assuming that the user has already configured Adam, aborting discovery.
162
- if self._async_current_entries() and _product == "smile_thermo":
163
- return self.async_abort(reason="anna_with_adam")
164
 
165
  # If we have discovered an Adam or Anna, both might be on the network.
166
  # In that case, we need to cancel the Anna flow, as the Adam should
@@ -168,12 +208,13 @@
168
  if self.hass.config_entries.flow.async_has_matching_flow(self):
169
  return self.async_abort(reason="anna_with_adam")
170
 
 
171
  self.context.update(
172
  {
173
- "title_placeholders": {CONF_NAME: _name},
174
  ATTR_CONFIGURATION_URL: (
175
  f"http://{discovery_info.host}:{discovery_info.port}"
176
- ),
177
  }
178
  )
179
  return await self.async_step_user()
@@ -181,15 +222,16 @@
181
  def is_matching(self, other_flow: Self) -> bool:
182
  """Return True if other_flow is matching this flow."""
183
  # This is an Anna, and there is already an Adam flow in progress
184
- if self.product == "smile_thermo" and other_flow.product == "smile_open_therm":
185
  return True
186
 
187
  # This is an Adam, and there is already an Anna flow in progress
188
- if self.product == "smile_open_therm" and other_flow.product == "smile_thermo":
189
  self.hass.config_entries.flow.async_abort(other_flow.flow_id)
190
 
191
  return False
192
 
 
193
  async def async_step_user(
194
  self, user_input: dict[str, Any] | None = None
195
  ) -> ConfigFlowResult:
@@ -205,18 +247,19 @@
205
  api, errors = await verify_connection(self.hass, user_input)
206
  if api:
207
  await self.async_set_unique_id(
208
- api.smile_hostname or api.gateway_id,
209
- raise_on_progress=False,
210
  )
211
  self._abort_if_unique_id_configured()
212
  return self.async_create_entry(title=api.smile_name, data=user_input)
213
 
 
214
  return self.async_show_form(
215
  step_id=SOURCE_USER,
216
- data_schema=smile_user_schema(self.discovery_info),
217
  errors=errors,
218
  )
219
 
 
220
  async def async_step_reconfigure(
221
  self, user_input: dict[str, Any] | None = None
222
  ) -> ConfigFlowResult:
@@ -226,7 +269,7 @@
226
  reconfigure_entry = self._get_reconfigure_entry()
227
 
228
  if user_input:
229
- # Keep current username and password
230
  full_input = {
231
  CONF_HOST: user_input.get(CONF_HOST),
232
  CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
@@ -237,8 +280,7 @@
237
  api, errors = await verify_connection(self.hass, full_input)
238
  if api:
239
  await self.async_set_unique_id(
240
- api.smile_hostname or api.gateway_id,
241
- raise_on_progress=False,
242
  )
243
  self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
244
  return self.async_update_reload_and_abort(
@@ -255,3 +297,69 @@
255
  description_placeholders={"title": reconfigure_entry.title},
256
  errors=errors,
257
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  from __future__ import annotations
4
 
5
+ from copy import deepcopy
6
  import logging
7
  from typing import Any, Self
8
 
 
17
  )
18
  import voluptuous as vol
19
 
20
+ from homeassistant.components.zeroconf import ZeroconfServiceInfo
21
+ from homeassistant.config_entries import (
22
+ SOURCE_USER,
23
+ ConfigEntry,
24
+ ConfigFlow,
25
+ ConfigFlowResult,
26
+ OptionsFlow,
27
+ )
28
+
29
+ # Upstream
30
  from homeassistant.const import (
31
  ATTR_CONFIGURATION_URL,
32
  CONF_BASE,
 
34
  CONF_NAME,
35
  CONF_PASSWORD,
36
  CONF_PORT,
37
+ CONF_SCAN_INTERVAL,
38
  CONF_USERNAME,
39
  )
40
+
41
+ # Upstream
42
+ from homeassistant.core import HomeAssistant, callback
43
+ from homeassistant.helpers import config_validation as cv
44
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
 
45
 
46
  from .const import (
47
+ ANNA_WITH_ADAM,
48
+ CONF_HOMEKIT_EMULATION, # pw-beta option
49
+ CONF_REFRESH_INTERVAL, # pw-beta option
50
  DEFAULT_PORT,
51
+ DEFAULT_SCAN_INTERVAL, # pw-beta option
52
  DEFAULT_USERNAME,
53
  DOMAIN,
54
  FLOW_SMILE,
55
  FLOW_STRETCH,
56
+ INIT,
57
  SMILE,
58
+ SMILE_OPEN_THERM,
59
+ SMILE_THERMO,
60
  STRETCH,
61
  STRETCH_USERNAME,
62
+ THERMOSTAT,
63
+ TITLE_PLACEHOLDERS,
64
+ VERSION,
65
  ZEROCONF_MAP,
66
  )
67
 
68
+ # Upstream
69
+ from .coordinator import PlugwiseDataUpdateCoordinator
70
+
71
+ type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
72
+
73
  _LOGGER = logging.getLogger(__name__)
74
 
75
+ # Upstream basically the whole file (excluding the pw-beta options)
76
+
77
  SMILE_RECONF_SCHEMA = vol.Schema(
78
  {
79
  vol.Required(CONF_HOST): str,
 
81
  )
82
 
83
 
84
+ def smile_user_schema(cf_input: ZeroconfServiceInfo | dict[str, Any] | None) -> vol.Schema:
85
  """Generate base schema for gateways."""
86
+ if not cf_input: # no discovery- or user-input available
87
+ return vol.Schema(
 
 
88
  {
89
  vol.Required(CONF_HOST): str,
90
+ vol.Required(CONF_PASSWORD): str,
91
  # Port under investigation for removal (hence not added in #132878)
92
  vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
93
  vol.Required(CONF_USERNAME, default=SMILE): vol.In(
 
96
  }
97
  )
98
 
99
+ if isinstance(cf_input, ZeroconfServiceInfo):
100
+ return vol.Schema({vol.Required(CONF_PASSWORD): str})
101
+
102
+ return vol.Schema(
103
+ {
104
+ vol.Required(CONF_HOST, default=cf_input[CONF_HOST]): str,
105
+ vol.Required(CONF_PASSWORD, default=cf_input[CONF_PASSWORD]): str,
106
+ vol.Optional(CONF_PORT, default=cf_input[CONF_PORT]): int,
107
+ vol.Required(CONF_USERNAME, default=cf_input[CONF_USERNAME]): vol.In(
108
+ {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
109
+ ),
110
+ }
111
+ )
112
 
113
 
114
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
 
146
  errors[CONF_BASE] = "response_error"
147
  except UnsupportedDeviceError:
148
  errors[CONF_BASE] = "unsupported"
149
+ except Exception: # noqa: BLE001
150
  _LOGGER.exception(
151
  "Unknown exception while verifying connection with your Plugwise Smile"
152
  )
 
158
  """Handle a config flow for Plugwise Smile."""
159
 
160
  VERSION = 1
161
+ MINOR_VERSION = 1
162
 
163
  discovery_info: ZeroconfServiceInfo | None = None
164
  product: str = "Unknown Smile"
 
170
  """Prepare configuration for a discovered Plugwise Smile."""
171
  self.discovery_info = discovery_info
172
  _properties = discovery_info.properties
173
+ _version = _properties.get(VERSION, "n/a")
174
+ self.product = _product = _properties.get("product", "Unknown Smile")
175
  unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
176
+ if DEFAULT_USERNAME not in unique_id:
177
+ self._username = STRETCH_USERNAME
178
+
179
  if config_entry := await self.async_set_unique_id(unique_id):
180
  try:
181
  await validate_input(
182
  self.hass,
183
  {
184
  CONF_HOST: discovery_info.host,
185
+ CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
186
  CONF_PORT: discovery_info.port,
187
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
 
188
  },
189
  )
190
  except Exception: # noqa: BLE001
 
197
  }
198
  )
199
 
 
 
 
 
 
 
200
  # This is an Anna, but we already have config entries.
201
  # Assuming that the user has already configured Adam, aborting discovery.
202
+ if self._async_current_entries() and _product == SMILE_THERMO:
203
+ return self.async_abort(reason=ANNA_WITH_ADAM)
204
 
205
  # If we have discovered an Adam or Anna, both might be on the network.
206
  # In that case, we need to cancel the Anna flow, as the Adam should
 
208
  if self.hass.config_entries.flow.async_has_matching_flow(self):
209
  return self.async_abort(reason="anna_with_adam")
210
 
211
+ _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
212
  self.context.update(
213
  {
214
+ TITLE_PLACEHOLDERS: {CONF_NAME: _name},
215
  ATTR_CONFIGURATION_URL: (
216
  f"http://{discovery_info.host}:{discovery_info.port}"
217
+ )
218
  }
219
  )
220
  return await self.async_step_user()
 
222
  def is_matching(self, other_flow: Self) -> bool:
223
  """Return True if other_flow is matching this flow."""
224
  # This is an Anna, and there is already an Adam flow in progress
225
+ if self.product == SMILE_THERMO and other_flow.product == SMILE_OPEN_THERM:
226
  return True
227
 
228
  # This is an Adam, and there is already an Anna flow in progress
229
+ if self.product == SMILE_OPEN_THERM and other_flow.product == SMILE_THERMO:
230
  self.hass.config_entries.flow.async_abort(other_flow.flow_id)
231
 
232
  return False
233
 
234
+
235
  async def async_step_user(
236
  self, user_input: dict[str, Any] | None = None
237
  ) -> ConfigFlowResult:
 
247
  api, errors = await verify_connection(self.hass, user_input)
248
  if api:
249
  await self.async_set_unique_id(
250
+ api.smile_hostname or api.gateway_id, raise_on_progress=False
 
251
  )
252
  self._abort_if_unique_id_configured()
253
  return self.async_create_entry(title=api.smile_name, data=user_input)
254
 
255
+ configure_input = self.discovery_info or user_input
256
  return self.async_show_form(
257
  step_id=SOURCE_USER,
258
+ data_schema=smile_user_schema(configure_input),
259
  errors=errors,
260
  )
261
 
262
+
263
  async def async_step_reconfigure(
264
  self, user_input: dict[str, Any] | None = None
265
  ) -> ConfigFlowResult:
 
269
  reconfigure_entry = self._get_reconfigure_entry()
270
 
271
  if user_input:
272
+ # Redefine ingest existing username and password
273
  full_input = {
274
  CONF_HOST: user_input.get(CONF_HOST),
275
  CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
 
280
  api, errors = await verify_connection(self.hass, full_input)
281
  if api:
282
  await self.async_set_unique_id(
283
+ api.smile_hostname or api.gateway_id, raise_on_progress=False
 
284
  )
285
  self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
286
  return self.async_update_reload_and_abort(
 
297
  description_placeholders={"title": reconfigure_entry.title},
298
  errors=errors,
299
  )
300
+
301
+
302
+ @staticmethod
303
+ @callback
304
+ def async_get_options_flow(
305
+ config_entry: PlugwiseConfigEntry,
306
+ ) -> PlugwiseOptionsFlowHandler: # pw-beta options
307
+ """Get the options flow for this handler."""
308
+ return PlugwiseOptionsFlowHandler(config_entry)
309
+
310
+
311
+ # pw-beta - change the scan-interval via CONFIGURE
312
+ # pw-beta - add homekit emulation via CONFIGURE
313
+ # pw-beta - change the frontend refresh interval via CONFIGURE
314
+ class PlugwiseOptionsFlowHandler(OptionsFlow): # pw-beta options
315
+ """Plugwise option flow."""
316
+
317
+ def __init__(self, config_entry: ConfigEntry) -> None:
318
+ """Initialize options flow."""
319
+ self.options = deepcopy(dict(config_entry.options))
320
+
321
+ def _create_options_schema(self, coordinator: PlugwiseDataUpdateCoordinator) -> vol.Schema:
322
+ interval = DEFAULT_SCAN_INTERVAL[coordinator.api.smile_type] # pw-beta options
323
+ schema = {
324
+ vol.Optional(
325
+ CONF_SCAN_INTERVAL,
326
+ default=self.options.get(CONF_SCAN_INTERVAL, interval.seconds),
327
+ ): vol.All(cv.positive_int, vol.Clamp(min=10)),
328
+ } # pw-beta
329
+
330
+ if coordinator.api.smile_type == THERMOSTAT:
331
+ schema.update({
332
+ vol.Optional(
333
+ CONF_HOMEKIT_EMULATION,
334
+ default=self.options.get(CONF_HOMEKIT_EMULATION, False),
335
+ ): vol.All(cv.boolean),
336
+ vol.Optional(
337
+ CONF_REFRESH_INTERVAL,
338
+ default=self.options.get(CONF_REFRESH_INTERVAL, 1.5),
339
+ ): vol.All(vol.Coerce(float), vol.Range(min=1.5, max=10.0)),
340
+ }) # pw-beta
341
+
342
+ return vol.Schema(schema)
343
+
344
+ async def async_step_none(
345
+ self, user_input: dict[str, Any] | None = None
346
+ ) -> ConfigFlowResult: # pragma: no cover
347
+ """No options available."""
348
+ if user_input is not None:
349
+ # Apparently not possible to abort an options flow at the moment
350
+ return self.async_create_entry(title="", data=self.options)
351
+ return self.async_show_form(step_id="none")
352
+
353
+ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
354
+ """Manage the Plugwise options."""
355
+ if not self.config_entry.data.get(CONF_HOST):
356
+ return await self.async_step_none(user_input) # pragma: no cover
357
+
358
+ if user_input is not None:
359
+ return self.async_create_entry(title="", data=user_input)
360
+
361
+ coordinator = self.config_entry.runtime_data
362
+ return self.async_show_form(
363
+ step_id=INIT,
364
+ data_schema=self._create_options_schema(coordinator)
365
+ )
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/const.py RENAMED
@@ -1,30 +1,167 @@
1
  """Constants for Plugwise component."""
2
 
3
- from __future__ import annotations
4
-
5
  from datetime import timedelta
6
  import logging
7
  from typing import Final, Literal
8
 
9
  from homeassistant.const import Platform
10
 
 
 
11
  DOMAIN: Final = "plugwise"
12
 
13
  LOGGER = logging.getLogger(__package__)
14
 
15
  API: Final = "api"
16
- FLOW_SMILE: Final = "smile (Adam/Anna/P1)"
17
- FLOW_STRETCH: Final = "stretch (Stretch)"
18
- FLOW_TYPE: Final = "flow_type"
 
19
  GATEWAY: Final = "gateway"
20
- GATEWAY_ID: Final = "gateway_id"
21
  LOCATION: Final = "location"
22
- PW_TYPE: Final = "plugwise_type"
23
  REBOOT: Final = "reboot"
24
  SMILE: Final = "smile"
25
  STRETCH: Final = "stretch"
26
  STRETCH_USERNAME: Final = "stretch"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
 
 
 
 
 
 
 
 
 
 
 
28
  PLATFORMS: Final[list[str]] = [
29
  Platform.BINARY_SENSOR,
30
  Platform.BUTTON,
@@ -34,6 +171,18 @@
34
  Platform.SENSOR,
35
  Platform.SWITCH,
36
  ]
 
 
 
 
 
 
 
 
 
 
 
 
37
  ZEROCONF_MAP: Final[dict[str, str]] = {
38
  "smile": "Smile P1",
39
  "smile_thermo": "Smile Anna",
@@ -59,21 +208,3 @@
59
  "regulation_modes",
60
  "available_schedules",
61
  ]
62
-
63
- # Default directives
64
- DEFAULT_MAX_TEMP: Final = 30
65
- DEFAULT_MIN_TEMP: Final = 4
66
- DEFAULT_PORT: Final = 80
67
- DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
68
- "power": timedelta(seconds=10),
69
- "stretch": timedelta(seconds=60),
70
- "thermostat": timedelta(seconds=60),
71
- }
72
- DEFAULT_USERNAME: Final = "smile"
73
-
74
- MASTER_THERMOSTATS: Final[list[str]] = [
75
- "thermostat",
76
- "thermostatic_radiator_valve",
77
- "zone_thermometer",
78
- "zone_thermostat",
79
- ]
 
1
  """Constants for Plugwise component."""
2
 
 
 
3
  from datetime import timedelta
4
  import logging
5
  from typing import Final, Literal
6
 
7
  from homeassistant.const import Platform
8
 
9
+ # 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
+ TEMP_DIFF: Final = "temperature_difference"
126
+ VALVE_POS: Final = "valve_position"
127
+ VOLTAGE_PH1: Final = "voltage_phase_one"
128
+ VOLTAGE_PH2: Final = "voltage_phase_two"
129
+ VOLTAGE_PH3: Final = "voltage_phase_three"
130
+ WATER_TEMP: Final = "water_temperature"
131
+ WATER_PRESSURE: Final = "water_pressure"
132
+
133
+ # Select constants
134
+ AVAILABLE_SCHEDULES: Final = "available_schedules"
135
+ DHW_MODE: Final = "dhw_mode"
136
+ DHW_MODES: Final = "dhw_modes"
137
+ GATEWAY_MODE: Final = "gateway_mode"
138
+ GATEWAY_MODES: Final = "gateway_modes"
139
+ REGULATION_MODE: Final = "regulation_mode"
140
+ REGULATION_MODES: Final = "regulation_modes"
141
+ SELECT_DHW_MODE: Final = "select_dhw_mode"
142
+ SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
143
+ SELECT_REGULATION_MODE: Final = "select_regulation_mode"
144
+ SELECT_SCHEDULE: Final = "select_schedule"
145
+
146
+ # Switch constants
147
+ DHW_CM_SWITCH: Final = "dhw_cm_switch"
148
+ LOCK: Final = "lock"
149
+ MEMBERS: Final ="members"
150
+ RELAY: Final = "relay"
151
+ COOLING_ENA_SWITCH: Final ="cooling_ena_switch"
152
+ SWITCHES: Final = "switches"
153
 
154
+ # Default directives
155
+ DEFAULT_PORT: Final[int] = 80
156
+ DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
157
+ "power": timedelta(seconds=10),
158
+ "stretch": timedelta(seconds=60),
159
+ "thermostat": timedelta(seconds=60),
160
+ }
161
+ DEFAULT_TIMEOUT: Final[int] = 30
162
+ DEFAULT_USERNAME: Final = "smile"
163
+
164
+ # --- Const for Plugwise Smile and Stretch
165
  PLATFORMS: Final[list[str]] = [
166
  Platform.BINARY_SENSOR,
167
  Platform.BUTTON,
 
171
  Platform.SENSOR,
172
  Platform.SWITCH,
173
  ]
174
+ SERVICE_DELETE: Final = "delete_notification"
175
+ SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
176
+
177
+ # Climate const:
178
+ MASTER_THERMOSTATS: Final[list[str]] = [
179
+ "thermostat",
180
+ "zone_thermometer",
181
+ "zone_thermostat",
182
+ "thermostatic_radiator_valve",
183
+ ]
184
+
185
+ # Config_flow const:
186
  ZEROCONF_MAP: Final[dict[str, str]] = {
187
  "smile": "Smile P1",
188
  "smile_thermo": "Smile Anna",
 
208
  "regulation_modes",
209
  "available_schedules",
210
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/coordinator.py RENAMED
@@ -2,7 +2,6 @@
2
 
3
  from datetime import timedelta
4
 
5
- from packaging.version import Version
6
  from plugwise import PlugwiseData, Smile
7
  from plugwise.exceptions import (
8
  ConnectionFailedError,
@@ -14,15 +13,22 @@
14
  )
15
 
16
  from homeassistant.config_entries import ConfigEntry
17
- from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
 
 
 
 
 
 
18
  from homeassistant.core import HomeAssistant
19
  from homeassistant.exceptions import ConfigEntryError
20
  from homeassistant.helpers import device_registry as dr
21
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
22
  from homeassistant.helpers.debounce import Debouncer
23
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
24
 
25
- from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, GATEWAY_ID, LOGGER
26
 
27
 
28
  class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
@@ -32,32 +38,40 @@
32
 
33
  config_entry: ConfigEntry
34
 
35
- def __init__(self, hass: HomeAssistant) -> None:
 
 
 
 
 
36
  """Initialize the coordinator."""
37
  super().__init__(
38
  hass,
39
  LOGGER,
40
  name=DOMAIN,
41
- update_interval=timedelta(seconds=60),
 
 
42
  # Don't refresh immediately, give the device time to process
43
  # the change in state before we query it.
44
  request_refresh_debouncer=Debouncer(
45
  hass,
46
  LOGGER,
47
- cooldown=1.5,
48
  immediate=False,
49
  ),
50
  )
51
 
52
  self.api = Smile(
53
  host=self.config_entry.data[CONF_HOST],
54
- username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
55
  password=self.config_entry.data[CONF_PASSWORD],
56
- port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
 
57
  websession=async_get_clientsession(hass, verify_ssl=False),
58
  )
59
  self._current_devices: set[str] = set()
60
  self.new_devices: set[str] = set()
 
61
 
62
  async def _connect(self) -> None:
63
  """Connect to the Plugwise Smile."""
@@ -65,9 +79,19 @@
65
  self._connected = isinstance(version, Version)
66
  if self._connected:
67
  self.api.get_all_gateway_entities()
 
 
 
 
 
 
 
 
 
68
 
69
  async def _async_update_data(self) -> PlugwiseData:
70
  """Fetch data from Plugwise."""
 
71
  try:
72
  if not self._connected:
73
  await self._connect()
@@ -83,6 +107,7 @@
83
  translation_key="authentication_failed",
84
  ) from err
85
  except (InvalidXMLError, ResponseError) as err:
 
86
  raise UpdateFailed(
87
  translation_domain=DOMAIN,
88
  translation_key="invalid_xml_data",
@@ -97,11 +122,13 @@
97
  translation_domain=DOMAIN,
98
  translation_key="unsupported_firmware",
99
  ) from err
 
 
 
100
 
101
- self._async_add_remove_devices(data, self.config_entry)
102
  return data
103
 
104
- def _async_add_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
105
  """Add new Plugwise devices, remove non-existing devices."""
106
  # Check for new or removed devices
107
  self.new_devices = set(data.devices) - self._current_devices
@@ -109,20 +136,19 @@
109
  self._current_devices = set(data.devices)
110
 
111
  if removed_devices:
112
- self._async_remove_devices(data, entry)
113
 
114
- def _async_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
115
  """Clean registries when removed devices found."""
116
  device_reg = dr.async_get(self.hass)
117
  device_list = dr.async_entries_for_config_entry(
118
  device_reg, self.config_entry.entry_id
119
  )
 
120
  # First find the Plugwise via_device
121
- gateway_device = device_reg.async_get_device(
122
- {(DOMAIN, data.gateway[GATEWAY_ID])}
123
- )
124
- assert gateway_device is not None
125
- via_device_id = gateway_device.id
126
 
127
  # Then remove the connected orphaned device(s)
128
  for device_entry in device_list:
@@ -136,7 +162,7 @@
136
  device_entry.id, remove_config_entry_id=entry.entry_id
137
  )
138
  LOGGER.debug(
139
- "Removed %s device %s %s from device_registry",
140
  DOMAIN,
141
  device_entry.model,
142
  identifier[1],
 
2
 
3
  from datetime import timedelta
4
 
 
5
  from plugwise import PlugwiseData, Smile
6
  from plugwise.exceptions import (
7
  ConnectionFailedError,
 
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]):
 
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."""
 
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()
 
107
  translation_key="authentication_failed",
108
  ) from err
109
  except (InvalidXMLError, ResponseError) as err:
110
+ # pwbeta TODO; we had {err} in the text, but not upstream, do we want this?
111
  raise UpdateFailed(
112
  translation_domain=DOMAIN,
113
  translation_key="invalid_xml_data",
 
122
  translation_domain=DOMAIN,
123
  translation_key="unsupported_firmware",
124
  ) from err
125
+ else:
126
+ LOGGER.debug(f"{self.api.smile_name} data: %s", data)
127
+ await self.async_add_remove_devices(data, self.config_entry)
128
 
 
129
  return data
130
 
131
+ async def async_add_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
132
  """Add new Plugwise devices, remove non-existing devices."""
133
  # Check for new or removed devices
134
  self.new_devices = set(data.devices) - self._current_devices
 
136
  self._current_devices = set(data.devices)
137
 
138
  if removed_devices:
139
+ await self.async_remove_devices(data, entry)
140
 
141
+ async def async_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
142
  """Clean registries when removed devices found."""
143
  device_reg = dr.async_get(self.hass)
144
  device_list = dr.async_entries_for_config_entry(
145
  device_reg, self.config_entry.entry_id
146
  )
147
+
148
  # First find the Plugwise via_device
149
+ gateway_device = device_reg.async_get_device({(DOMAIN, data.gateway[GATEWAY_ID])})
150
+ if gateway_device is not None:
151
+ via_device_id = gateway_device.id
 
 
152
 
153
  # Then remove the connected orphaned device(s)
154
  for device_entry in device_list:
 
162
  device_entry.id, remove_config_entry_id=entry.entry_id
163
  )
164
  LOGGER.debug(
165
+ "Removed %s device/zone %s %s from device_registry",
166
  DOMAIN,
167
  device_entry.model,
168
  identifier[1],
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/entity.py RENAMED
@@ -12,7 +12,21 @@
12
  )
13
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
14
 
15
- from .const import DOMAIN
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  from .coordinator import PlugwiseDataUpdateCoordinator
17
 
18
 
@@ -36,30 +50,30 @@
36
 
37
  data = coordinator.data.devices[device_id]
38
  connections = set()
39
- if mac := data.get("mac_address"):
40
  connections.add((CONNECTION_NETWORK_MAC, mac))
41
- if mac := data.get("zigbee_mac_address"):
42
  connections.add((CONNECTION_ZIGBEE, mac))
43
 
44
  self._attr_device_info = DeviceInfo(
45
  configuration_url=configuration_url,
46
  identifiers={(DOMAIN, device_id)},
47
  connections=connections,
48
- manufacturer=data.get("vendor"),
49
- model=data.get("model"),
50
- model_id=data.get("model_id"),
51
- name=coordinator.data.gateway["smile_name"],
52
- sw_version=data.get("firmware"),
53
- hw_version=data.get("hardware"),
54
  )
55
 
56
- if device_id != coordinator.data.gateway["gateway_id"]:
57
  self._attr_device_info.update(
58
  {
59
- ATTR_NAME: data.get("name"),
60
  ATTR_VIA_DEVICE: (
61
  DOMAIN,
62
- str(self.coordinator.data.gateway["gateway_id"]),
63
  ),
64
  }
65
  )
@@ -68,8 +82,10 @@
68
  def available(self) -> bool:
69
  """Return if entity is available."""
70
  return (
 
 
71
  self._dev_id in self.coordinator.data.devices
72
- and ("available" not in self.device or self.device["available"] is True)
73
  and super().available
74
  )
75
 
 
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
 
 
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
  )
 
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
  )
91
 
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/icons.json RENAMED
@@ -120,5 +120,8 @@
120
  "default": "mdi:lock"
121
  }
122
  }
 
 
 
123
  }
124
  }
 
120
  "default": "mdi:lock"
121
  }
122
  }
123
+ },
124
+ "services": {
125
+ "delete_notification": "mdi:trash-can"
126
  }
127
  }
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/manifest.json RENAMED
@@ -1,12 +1,13 @@
1
  {
2
  "domain": "plugwise",
3
- "name": "Plugwise",
4
  "codeowners": ["@CoMPaTech", "@bouwew"],
5
  "config_flow": true,
6
- "documentation": "https://www.home-assistant.io/integrations/plugwise",
7
  "integration_type": "hub",
8
  "iot_class": "local_polling",
9
  "loggers": ["plugwise"],
10
  "requirements": ["plugwise==1.6.4"],
 
11
  "zeroconf": ["_plugwise._tcp.local."]
12
  }
 
1
  {
2
  "domain": "plugwise",
3
+ "name": "Plugwise Beta",
4
  "codeowners": ["@CoMPaTech", "@bouwew"],
5
  "config_flow": true,
6
+ "documentation": "https://github.com/plugwise/plugwise-beta",
7
  "integration_type": "hub",
8
  "iot_class": "local_polling",
9
  "loggers": ["plugwise"],
10
  "requirements": ["plugwise==1.6.4"],
11
+ "version": "0.55.5",
12
  "zeroconf": ["_plugwise._tcp.local."]
13
  }
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/number.py RENAMED
@@ -15,7 +15,18 @@
15
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
16
 
17
  from . import PlugwiseConfigEntry
18
- from .const import NumberType
 
 
 
 
 
 
 
 
 
 
 
19
  from .coordinator import PlugwiseDataUpdateCoordinator
20
  from .entity import PlugwiseEntity
21
  from .util import plugwise_command
@@ -30,24 +41,25 @@
30
  key: NumberType
31
 
32
 
 
33
  NUMBER_TYPES = (
34
  PlugwiseNumberEntityDescription(
35
- key="maximum_boiler_temperature",
36
- translation_key="maximum_boiler_temperature",
37
  device_class=NumberDeviceClass.TEMPERATURE,
38
  entity_category=EntityCategory.CONFIG,
39
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
40
  ),
41
  PlugwiseNumberEntityDescription(
42
- key="max_dhw_temperature",
43
- translation_key="max_dhw_temperature",
44
  device_class=NumberDeviceClass.TEMPERATURE,
45
  entity_category=EntityCategory.CONFIG,
46
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
47
  ),
48
  PlugwiseNumberEntityDescription(
49
- key="temperature_offset",
50
- translation_key="temperature_offset",
51
  device_class=NumberDeviceClass.TEMPERATURE,
52
  entity_category=EntityCategory.CONFIG,
53
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -60,7 +72,8 @@
60
  entry: PlugwiseConfigEntry,
61
  async_add_entities: AddEntitiesCallback,
62
  ) -> None:
63
- """Set up Plugwise number platform."""
 
64
  coordinator = entry.runtime_data
65
 
66
  @callback
@@ -69,12 +82,28 @@
69
  if not coordinator.new_devices:
70
  return
71
 
72
- async_add_entities(
73
- PlugwiseNumberEntity(coordinator, device_id, description)
74
- for device_id in coordinator.new_devices
75
- for description in NUMBER_TYPES
76
- if description.key in coordinator.data.devices[device_id]
77
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  _add_entities()
80
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -93,15 +122,16 @@
93
  ) -> None:
94
  """Initiate Plugwise Number."""
95
  super().__init__(coordinator, device_id)
96
- self._attr_mode = NumberMode.BOX
97
- self._attr_native_max_value = self.device[description.key]["upper_bound"]
98
- self._attr_native_min_value = self.device[description.key]["lower_bound"]
99
- self._attr_unique_id = f"{device_id}-{description.key}"
100
  self.device_id = device_id
101
  self.entity_description = description
 
 
 
 
102
 
103
- native_step = self.device[description.key]["resolution"]
104
- if description.key != "temperature_offset":
105
  native_step = max(native_step, 0.5)
106
  self._attr_native_step = native_step
107
 
@@ -113,6 +143,7 @@
113
  @plugwise_command
114
  async def async_set_native_value(self, value: float) -> None:
115
  """Change to the new setpoint value."""
116
- await self.coordinator.api.set_number(
117
- self.device_id, self.entity_description.key, value
 
118
  )
 
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
 
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,
 
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
  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
  ) -> 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
 
 
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
  )
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/select.py RENAMED
@@ -10,7 +10,25 @@
10
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
11
 
12
  from . import PlugwiseConfigEntry
13
- from .const import SelectOptionsType, SelectType
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  from .coordinator import PlugwiseDataUpdateCoordinator
15
  from .entity import PlugwiseEntity
16
  from .util import plugwise_command
@@ -26,29 +44,30 @@
26
  options_key: SelectOptionsType
27
 
28
 
 
29
  SELECT_TYPES = (
30
  PlugwiseSelectEntityDescription(
31
- key="select_schedule",
32
- translation_key="select_schedule",
33
- options_key="available_schedules",
34
  ),
35
  PlugwiseSelectEntityDescription(
36
- key="select_regulation_mode",
37
- translation_key="regulation_mode",
38
  entity_category=EntityCategory.CONFIG,
39
- options_key="regulation_modes",
40
  ),
41
  PlugwiseSelectEntityDescription(
42
- key="select_dhw_mode",
43
- translation_key="dhw_mode",
44
  entity_category=EntityCategory.CONFIG,
45
- options_key="dhw_modes",
46
  ),
47
  PlugwiseSelectEntityDescription(
48
- key="select_gateway_mode",
49
- translation_key="gateway_mode",
50
  entity_category=EntityCategory.CONFIG,
51
- options_key="gateway_modes",
52
  ),
53
  )
54
 
@@ -58,7 +77,7 @@
58
  entry: PlugwiseConfigEntry,
59
  async_add_entities: AddEntitiesCallback,
60
  ) -> None:
61
- """Set up the Smile selector from a config entry."""
62
  coordinator = entry.runtime_data
63
 
64
  @callback
@@ -67,12 +86,27 @@
67
  if not coordinator.new_devices:
68
  return
69
 
70
- async_add_entities(
71
- PlugwiseSelectEntity(coordinator, device_id, description)
72
- for device_id in coordinator.new_devices
73
- for description in SELECT_TYPES
74
- if description.options_key in coordinator.data.devices[device_id]
75
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  _add_entities()
78
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -95,7 +129,7 @@
95
  self.entity_description = entity_description
96
 
97
  self._location = device_id
98
- if (location := self.device.get("location")) is not None:
99
  self._location = location
100
 
101
  @property
@@ -112,8 +146,13 @@
112
  async def async_select_option(self, option: str) -> None:
113
  """Change to the selected entity option.
114
 
115
- self._location and STATE_ON are required for the thermostat-schedule select.
116
  """
117
  await self.coordinator.api.set_select(
118
  self.entity_description.key, self._location, option, STATE_ON
119
  )
 
 
 
 
 
 
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
 
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
 
 
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
  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))
 
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
 
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
+ )
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/sensor.py RENAMED
@@ -13,6 +13,7 @@
13
  SensorStateClass,
14
  )
15
  from homeassistant.const import (
 
16
  LIGHT_LUX,
17
  PERCENTAGE,
18
  EntityCategory,
@@ -28,6 +29,57 @@
28
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
29
 
30
  from . import PlugwiseConfigEntry
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  from .coordinator import PlugwiseDataUpdateCoordinator
32
  from .entity import PlugwiseEntity
33
 
@@ -42,17 +94,18 @@
42
  key: SensorType
43
 
44
 
45
- SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
 
46
  PlugwiseSensorEntityDescription(
47
- key="setpoint",
48
- translation_key="setpoint",
49
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
50
  device_class=SensorDeviceClass.TEMPERATURE,
51
  state_class=SensorStateClass.MEASUREMENT,
52
  entity_category=EntityCategory.DIAGNOSTIC,
53
  ),
54
  PlugwiseSensorEntityDescription(
55
- key="setpoint_high",
56
  translation_key="cooling_setpoint",
57
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
58
  device_class=SensorDeviceClass.TEMPERATURE,
@@ -60,7 +113,7 @@
60
  entity_category=EntityCategory.DIAGNOSTIC,
61
  ),
62
  PlugwiseSensorEntityDescription(
63
- key="setpoint_low",
64
  translation_key="heating_setpoint",
65
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
66
  device_class=SensorDeviceClass.TEMPERATURE,
@@ -68,276 +121,277 @@
68
  entity_category=EntityCategory.DIAGNOSTIC,
69
  ),
70
  PlugwiseSensorEntityDescription(
71
- key="temperature",
72
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
73
  device_class=SensorDeviceClass.TEMPERATURE,
74
  entity_category=EntityCategory.DIAGNOSTIC,
75
  state_class=SensorStateClass.MEASUREMENT,
76
  ),
77
  PlugwiseSensorEntityDescription(
78
- key="intended_boiler_temperature",
79
- translation_key="intended_boiler_temperature",
80
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
81
  device_class=SensorDeviceClass.TEMPERATURE,
82
  entity_category=EntityCategory.DIAGNOSTIC,
83
  state_class=SensorStateClass.MEASUREMENT,
84
  ),
85
  PlugwiseSensorEntityDescription(
86
- key="temperature_difference",
87
- translation_key="temperature_difference",
88
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
89
  device_class=SensorDeviceClass.TEMPERATURE,
90
  entity_category=EntityCategory.DIAGNOSTIC,
91
  state_class=SensorStateClass.MEASUREMENT,
92
  ),
93
  PlugwiseSensorEntityDescription(
94
- key="outdoor_temperature",
95
- translation_key="outdoor_temperature",
96
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
97
  device_class=SensorDeviceClass.TEMPERATURE,
98
  state_class=SensorStateClass.MEASUREMENT,
 
99
  ),
100
  PlugwiseSensorEntityDescription(
101
- key="outdoor_air_temperature",
102
- translation_key="outdoor_air_temperature",
103
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
104
  device_class=SensorDeviceClass.TEMPERATURE,
105
  entity_category=EntityCategory.DIAGNOSTIC,
106
  state_class=SensorStateClass.MEASUREMENT,
107
  ),
108
  PlugwiseSensorEntityDescription(
109
- key="water_temperature",
110
- translation_key="water_temperature",
111
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
112
  device_class=SensorDeviceClass.TEMPERATURE,
113
  entity_category=EntityCategory.DIAGNOSTIC,
114
  state_class=SensorStateClass.MEASUREMENT,
115
  ),
116
  PlugwiseSensorEntityDescription(
117
- key="return_temperature",
118
- translation_key="return_temperature",
119
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
120
  device_class=SensorDeviceClass.TEMPERATURE,
121
  entity_category=EntityCategory.DIAGNOSTIC,
122
  state_class=SensorStateClass.MEASUREMENT,
123
  ),
124
  PlugwiseSensorEntityDescription(
125
- key="electricity_consumed",
126
- translation_key="electricity_consumed",
127
  native_unit_of_measurement=UnitOfPower.WATT,
128
  device_class=SensorDeviceClass.POWER,
129
  state_class=SensorStateClass.MEASUREMENT,
130
  ),
131
  PlugwiseSensorEntityDescription(
132
- key="electricity_produced",
133
- translation_key="electricity_produced",
134
  native_unit_of_measurement=UnitOfPower.WATT,
135
  device_class=SensorDeviceClass.POWER,
136
  state_class=SensorStateClass.MEASUREMENT,
137
  entity_registry_enabled_default=False,
138
  ),
139
  PlugwiseSensorEntityDescription(
140
- key="electricity_consumed_interval",
141
- translation_key="electricity_consumed_interval",
142
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
143
  device_class=SensorDeviceClass.ENERGY,
144
  state_class=SensorStateClass.TOTAL,
145
  ),
146
  PlugwiseSensorEntityDescription(
147
- key="electricity_consumed_peak_interval",
148
- translation_key="electricity_consumed_peak_interval",
149
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
150
  device_class=SensorDeviceClass.ENERGY,
151
  state_class=SensorStateClass.TOTAL,
152
  ),
153
  PlugwiseSensorEntityDescription(
154
- key="electricity_consumed_off_peak_interval",
155
- translation_key="electricity_consumed_off_peak_interval",
156
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
157
  device_class=SensorDeviceClass.ENERGY,
158
  state_class=SensorStateClass.TOTAL,
159
  ),
160
  PlugwiseSensorEntityDescription(
161
- key="electricity_produced_interval",
162
- translation_key="electricity_produced_interval",
163
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
164
  device_class=SensorDeviceClass.ENERGY,
165
  state_class=SensorStateClass.TOTAL,
166
  entity_registry_enabled_default=False,
167
  ),
168
  PlugwiseSensorEntityDescription(
169
- key="electricity_produced_peak_interval",
170
- translation_key="electricity_produced_peak_interval",
171
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
172
  device_class=SensorDeviceClass.ENERGY,
173
  state_class=SensorStateClass.TOTAL,
174
  ),
175
  PlugwiseSensorEntityDescription(
176
- key="electricity_produced_off_peak_interval",
177
- translation_key="electricity_produced_off_peak_interval",
178
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
179
  device_class=SensorDeviceClass.ENERGY,
180
  state_class=SensorStateClass.TOTAL,
181
  ),
182
  PlugwiseSensorEntityDescription(
183
- key="electricity_consumed_point",
184
- translation_key="electricity_consumed_point",
185
  device_class=SensorDeviceClass.POWER,
186
  native_unit_of_measurement=UnitOfPower.WATT,
187
  state_class=SensorStateClass.MEASUREMENT,
188
  ),
189
  PlugwiseSensorEntityDescription(
190
- key="electricity_consumed_off_peak_point",
191
- translation_key="electricity_consumed_off_peak_point",
192
  native_unit_of_measurement=UnitOfPower.WATT,
193
  device_class=SensorDeviceClass.POWER,
194
  state_class=SensorStateClass.MEASUREMENT,
195
  ),
196
  PlugwiseSensorEntityDescription(
197
- key="electricity_consumed_peak_point",
198
- translation_key="electricity_consumed_peak_point",
199
  native_unit_of_measurement=UnitOfPower.WATT,
200
  device_class=SensorDeviceClass.POWER,
201
  state_class=SensorStateClass.MEASUREMENT,
202
  ),
203
  PlugwiseSensorEntityDescription(
204
- key="electricity_consumed_off_peak_cumulative",
205
- translation_key="electricity_consumed_off_peak_cumulative",
206
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
207
  device_class=SensorDeviceClass.ENERGY,
208
  state_class=SensorStateClass.TOTAL_INCREASING,
209
  ),
210
  PlugwiseSensorEntityDescription(
211
- key="electricity_consumed_peak_cumulative",
212
- translation_key="electricity_consumed_peak_cumulative",
213
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
214
  device_class=SensorDeviceClass.ENERGY,
215
  state_class=SensorStateClass.TOTAL_INCREASING,
216
  ),
217
  PlugwiseSensorEntityDescription(
218
- key="electricity_produced_point",
219
- translation_key="electricity_produced_point",
220
  device_class=SensorDeviceClass.POWER,
221
  native_unit_of_measurement=UnitOfPower.WATT,
222
  state_class=SensorStateClass.MEASUREMENT,
223
  ),
224
  PlugwiseSensorEntityDescription(
225
- key="electricity_produced_off_peak_point",
226
- translation_key="electricity_produced_off_peak_point",
227
  native_unit_of_measurement=UnitOfPower.WATT,
228
  device_class=SensorDeviceClass.POWER,
229
  state_class=SensorStateClass.MEASUREMENT,
230
  ),
231
  PlugwiseSensorEntityDescription(
232
- key="electricity_produced_peak_point",
233
- translation_key="electricity_produced_peak_point",
234
  native_unit_of_measurement=UnitOfPower.WATT,
235
  device_class=SensorDeviceClass.POWER,
236
  state_class=SensorStateClass.MEASUREMENT,
237
  ),
238
  PlugwiseSensorEntityDescription(
239
- key="electricity_produced_off_peak_cumulative",
240
- translation_key="electricity_produced_off_peak_cumulative",
241
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
242
  device_class=SensorDeviceClass.ENERGY,
243
  state_class=SensorStateClass.TOTAL_INCREASING,
244
  ),
245
  PlugwiseSensorEntityDescription(
246
- key="electricity_produced_peak_cumulative",
247
- translation_key="electricity_produced_peak_cumulative",
248
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
249
  device_class=SensorDeviceClass.ENERGY,
250
  state_class=SensorStateClass.TOTAL_INCREASING,
251
  ),
252
  PlugwiseSensorEntityDescription(
253
- key="electricity_phase_one_consumed",
254
- translation_key="electricity_phase_one_consumed",
255
  device_class=SensorDeviceClass.POWER,
256
  native_unit_of_measurement=UnitOfPower.WATT,
257
  state_class=SensorStateClass.MEASUREMENT,
258
  ),
259
  PlugwiseSensorEntityDescription(
260
- key="electricity_phase_two_consumed",
261
- translation_key="electricity_phase_two_consumed",
262
  device_class=SensorDeviceClass.POWER,
263
  native_unit_of_measurement=UnitOfPower.WATT,
264
  state_class=SensorStateClass.MEASUREMENT,
265
  ),
266
  PlugwiseSensorEntityDescription(
267
- key="electricity_phase_three_consumed",
268
- translation_key="electricity_phase_three_consumed",
269
  device_class=SensorDeviceClass.POWER,
270
  native_unit_of_measurement=UnitOfPower.WATT,
271
  state_class=SensorStateClass.MEASUREMENT,
272
  ),
273
  PlugwiseSensorEntityDescription(
274
- key="electricity_phase_one_produced",
275
- translation_key="electricity_phase_one_produced",
276
  device_class=SensorDeviceClass.POWER,
277
  native_unit_of_measurement=UnitOfPower.WATT,
278
  state_class=SensorStateClass.MEASUREMENT,
279
  ),
280
  PlugwiseSensorEntityDescription(
281
- key="electricity_phase_two_produced",
282
- translation_key="electricity_phase_two_produced",
283
  device_class=SensorDeviceClass.POWER,
284
  native_unit_of_measurement=UnitOfPower.WATT,
285
  state_class=SensorStateClass.MEASUREMENT,
286
  ),
287
  PlugwiseSensorEntityDescription(
288
- key="electricity_phase_three_produced",
289
- translation_key="electricity_phase_three_produced",
290
  device_class=SensorDeviceClass.POWER,
291
  native_unit_of_measurement=UnitOfPower.WATT,
292
  state_class=SensorStateClass.MEASUREMENT,
293
  ),
294
  PlugwiseSensorEntityDescription(
295
- key="voltage_phase_one",
296
- translation_key="voltage_phase_one",
297
  device_class=SensorDeviceClass.VOLTAGE,
298
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
299
  state_class=SensorStateClass.MEASUREMENT,
300
  entity_registry_enabled_default=False,
301
  ),
302
  PlugwiseSensorEntityDescription(
303
- key="voltage_phase_two",
304
- translation_key="voltage_phase_two",
305
  device_class=SensorDeviceClass.VOLTAGE,
306
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
307
  state_class=SensorStateClass.MEASUREMENT,
308
  entity_registry_enabled_default=False,
309
  ),
310
  PlugwiseSensorEntityDescription(
311
- key="voltage_phase_three",
312
- translation_key="voltage_phase_three",
313
  device_class=SensorDeviceClass.VOLTAGE,
314
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
315
  state_class=SensorStateClass.MEASUREMENT,
316
  entity_registry_enabled_default=False,
317
  ),
318
  PlugwiseSensorEntityDescription(
319
- key="gas_consumed_interval",
320
- translation_key="gas_consumed_interval",
321
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
322
  state_class=SensorStateClass.MEASUREMENT,
323
  ),
324
  PlugwiseSensorEntityDescription(
325
- key="gas_consumed_cumulative",
326
- translation_key="gas_consumed_cumulative",
327
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
328
  device_class=SensorDeviceClass.GAS,
329
  state_class=SensorStateClass.TOTAL,
330
  ),
331
  PlugwiseSensorEntityDescription(
332
- key="net_electricity_point",
333
- translation_key="net_electricity_point",
334
  native_unit_of_measurement=UnitOfPower.WATT,
335
  device_class=SensorDeviceClass.POWER,
336
  state_class=SensorStateClass.MEASUREMENT,
337
  ),
338
  PlugwiseSensorEntityDescription(
339
- key="net_electricity_cumulative",
340
- translation_key="net_electricity_cumulative",
341
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
342
  device_class=SensorDeviceClass.ENERGY,
343
  state_class=SensorStateClass.TOTAL,
@@ -357,22 +411,22 @@
357
  entity_category=EntityCategory.DIAGNOSTIC,
358
  ),
359
  PlugwiseSensorEntityDescription(
360
- key="modulation_level",
361
- translation_key="modulation_level",
362
  native_unit_of_measurement=PERCENTAGE,
363
  entity_category=EntityCategory.DIAGNOSTIC,
364
  state_class=SensorStateClass.MEASUREMENT,
365
  ),
366
  PlugwiseSensorEntityDescription(
367
- key="valve_position",
368
- translation_key="valve_position",
369
  entity_category=EntityCategory.DIAGNOSTIC,
370
  native_unit_of_measurement=PERCENTAGE,
371
  state_class=SensorStateClass.MEASUREMENT,
372
  ),
373
  PlugwiseSensorEntityDescription(
374
- key="water_pressure",
375
- translation_key="water_pressure",
376
  native_unit_of_measurement=UnitOfPressure.BAR,
377
  device_class=SensorDeviceClass.PRESSURE,
378
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -385,16 +439,16 @@
385
  state_class=SensorStateClass.MEASUREMENT,
386
  ),
387
  PlugwiseSensorEntityDescription(
388
- key="dhw_temperature",
389
- translation_key="dhw_temperature",
390
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
391
  device_class=SensorDeviceClass.TEMPERATURE,
392
  entity_category=EntityCategory.DIAGNOSTIC,
393
  state_class=SensorStateClass.MEASUREMENT,
394
  ),
395
  PlugwiseSensorEntityDescription(
396
- key="domestic_hot_water_setpoint",
397
- translation_key="domestic_hot_water_setpoint",
398
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
399
  device_class=SensorDeviceClass.TEMPERATURE,
400
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -408,7 +462,8 @@
408
  entry: PlugwiseConfigEntry,
409
  async_add_entities: AddEntitiesCallback,
410
  ) -> None:
411
- """Set up the Smile sensors from a config entry."""
 
412
  coordinator = entry.runtime_data
413
 
414
  @callback
@@ -417,13 +472,29 @@
417
  if not coordinator.new_devices:
418
  return
419
 
420
- async_add_entities(
421
- PlugwiseSensorEntity(coordinator, device_id, description)
422
- for device_id in coordinator.new_devices
423
- if (sensors := coordinator.data.devices[device_id].get("sensors"))
424
- for description in SENSORS
425
- if description.key in sensors
426
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
 
428
  _add_entities()
429
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -442,10 +513,10 @@
442
  ) -> None:
443
  """Initialise the sensor."""
444
  super().__init__(coordinator, device_id)
445
- self._attr_unique_id = f"{device_id}-{description.key}"
446
  self.entity_description = description
 
447
 
448
  @property
449
  def native_value(self) -> int | float:
450
  """Return the value reported by the sensor."""
451
- return self.device["sensors"][self.entity_description.key]
 
13
  SensorStateClass,
14
  )
15
  from homeassistant.const import (
16
+ ATTR_TEMPERATURE, # Upstream
17
  LIGHT_LUX,
18
  PERCENTAGE,
19
  EntityCategory,
 
29
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
30
 
31
  from . import PlugwiseConfigEntry
32
+ from .const import (
33
+ DHW_SETPOINT,
34
+ DHW_TEMP,
35
+ EL_CONS_INTERVAL,
36
+ EL_CONS_OP_CUMULATIVE,
37
+ EL_CONS_OP_INTERVAL,
38
+ EL_CONS_OP_POINT,
39
+ EL_CONS_P_CUMULATIVE,
40
+ EL_CONS_P_INTERVAL,
41
+ EL_CONS_P_POINT,
42
+ EL_CONS_POINT,
43
+ EL_CONSUMED,
44
+ EL_PH1_CONSUMED,
45
+ EL_PH1_PRODUCED,
46
+ EL_PH2_CONSUMED,
47
+ EL_PH2_PRODUCED,
48
+ EL_PH3_CONSUMED,
49
+ EL_PH3_PRODUCED,
50
+ EL_PROD_INTERVAL,
51
+ EL_PROD_OP_CUMULATIVE,
52
+ EL_PROD_OP_INTERVAL,
53
+ EL_PROD_OP_POINT,
54
+ EL_PROD_P_CUMULATIVE,
55
+ EL_PROD_P_INTERVAL,
56
+ EL_PROD_P_POINT,
57
+ EL_PROD_POINT,
58
+ EL_PRODUCED,
59
+ GAS_CONS_CUMULATIVE,
60
+ GAS_CONS_INTERVAL,
61
+ INTENDED_BOILER_TEMP,
62
+ LOGGER, # pw-beta
63
+ MOD_LEVEL,
64
+ NET_EL_CUMULATIVE,
65
+ NET_EL_POINT,
66
+ OUTDOOR_AIR_TEMP,
67
+ OUTDOOR_TEMP,
68
+ RETURN_TEMP,
69
+ SENSORS,
70
+ TARGET_TEMP,
71
+ TARGET_TEMP_HIGH,
72
+ TARGET_TEMP_LOW,
73
+ TEMP_DIFF,
74
+ VALVE_POS,
75
+ VOLTAGE_PH1,
76
+ VOLTAGE_PH2,
77
+ VOLTAGE_PH3,
78
+ WATER_PRESSURE,
79
+ WATER_TEMP,
80
+ )
81
+
82
+ # Upstream consts
83
  from .coordinator import PlugwiseDataUpdateCoordinator
84
  from .entity import PlugwiseEntity
85
 
 
94
  key: SensorType
95
 
96
 
97
+ # Upstream consts
98
+ PLUGWISE_SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
99
  PlugwiseSensorEntityDescription(
100
+ key=TARGET_TEMP,
101
+ translation_key=TARGET_TEMP,
102
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
103
  device_class=SensorDeviceClass.TEMPERATURE,
104
  state_class=SensorStateClass.MEASUREMENT,
105
  entity_category=EntityCategory.DIAGNOSTIC,
106
  ),
107
  PlugwiseSensorEntityDescription(
108
+ key=TARGET_TEMP_HIGH,
109
  translation_key="cooling_setpoint",
110
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
111
  device_class=SensorDeviceClass.TEMPERATURE,
 
113
  entity_category=EntityCategory.DIAGNOSTIC,
114
  ),
115
  PlugwiseSensorEntityDescription(
116
+ key=TARGET_TEMP_LOW,
117
  translation_key="heating_setpoint",
118
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
119
  device_class=SensorDeviceClass.TEMPERATURE,
 
121
  entity_category=EntityCategory.DIAGNOSTIC,
122
  ),
123
  PlugwiseSensorEntityDescription(
124
+ key=ATTR_TEMPERATURE,
125
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
126
  device_class=SensorDeviceClass.TEMPERATURE,
127
  entity_category=EntityCategory.DIAGNOSTIC,
128
  state_class=SensorStateClass.MEASUREMENT,
129
  ),
130
  PlugwiseSensorEntityDescription(
131
+ key=INTENDED_BOILER_TEMP,
132
+ translation_key=INTENDED_BOILER_TEMP,
133
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
134
  device_class=SensorDeviceClass.TEMPERATURE,
135
  entity_category=EntityCategory.DIAGNOSTIC,
136
  state_class=SensorStateClass.MEASUREMENT,
137
  ),
138
  PlugwiseSensorEntityDescription(
139
+ key=TEMP_DIFF,
140
+ translation_key=TEMP_DIFF,
141
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
142
  device_class=SensorDeviceClass.TEMPERATURE,
143
  entity_category=EntityCategory.DIAGNOSTIC,
144
  state_class=SensorStateClass.MEASUREMENT,
145
  ),
146
  PlugwiseSensorEntityDescription(
147
+ key=OUTDOOR_TEMP,
148
+ translation_key=OUTDOOR_TEMP,
149
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
150
  device_class=SensorDeviceClass.TEMPERATURE,
151
  state_class=SensorStateClass.MEASUREMENT,
152
+ suggested_display_precision=1,
153
  ),
154
  PlugwiseSensorEntityDescription(
155
+ key=OUTDOOR_AIR_TEMP,
156
+ translation_key=OUTDOOR_AIR_TEMP,
157
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
158
  device_class=SensorDeviceClass.TEMPERATURE,
159
  entity_category=EntityCategory.DIAGNOSTIC,
160
  state_class=SensorStateClass.MEASUREMENT,
161
  ),
162
  PlugwiseSensorEntityDescription(
163
+ key=WATER_TEMP,
164
+ translation_key=WATER_TEMP,
165
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
166
  device_class=SensorDeviceClass.TEMPERATURE,
167
  entity_category=EntityCategory.DIAGNOSTIC,
168
  state_class=SensorStateClass.MEASUREMENT,
169
  ),
170
  PlugwiseSensorEntityDescription(
171
+ key=RETURN_TEMP,
172
+ translation_key=RETURN_TEMP,
173
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
174
  device_class=SensorDeviceClass.TEMPERATURE,
175
  entity_category=EntityCategory.DIAGNOSTIC,
176
  state_class=SensorStateClass.MEASUREMENT,
177
  ),
178
  PlugwiseSensorEntityDescription(
179
+ key=EL_CONSUMED,
180
+ translation_key=EL_CONSUMED,
181
  native_unit_of_measurement=UnitOfPower.WATT,
182
  device_class=SensorDeviceClass.POWER,
183
  state_class=SensorStateClass.MEASUREMENT,
184
  ),
185
  PlugwiseSensorEntityDescription(
186
+ key=EL_PRODUCED,
187
+ translation_key=EL_PRODUCED,
188
  native_unit_of_measurement=UnitOfPower.WATT,
189
  device_class=SensorDeviceClass.POWER,
190
  state_class=SensorStateClass.MEASUREMENT,
191
  entity_registry_enabled_default=False,
192
  ),
193
  PlugwiseSensorEntityDescription(
194
+ key=EL_CONS_INTERVAL,
195
+ translation_key=EL_CONS_INTERVAL,
196
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
197
  device_class=SensorDeviceClass.ENERGY,
198
  state_class=SensorStateClass.TOTAL,
199
  ),
200
  PlugwiseSensorEntityDescription(
201
+ key=EL_CONS_P_INTERVAL,
202
+ translation_key=EL_CONS_P_INTERVAL,
203
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
204
  device_class=SensorDeviceClass.ENERGY,
205
  state_class=SensorStateClass.TOTAL,
206
  ),
207
  PlugwiseSensorEntityDescription(
208
+ key=EL_CONS_OP_INTERVAL,
209
+ translation_key=EL_CONS_OP_INTERVAL,
210
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
211
  device_class=SensorDeviceClass.ENERGY,
212
  state_class=SensorStateClass.TOTAL,
213
  ),
214
  PlugwiseSensorEntityDescription(
215
+ key=EL_PROD_INTERVAL,
216
+ translation_key=EL_PROD_INTERVAL,
217
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
218
  device_class=SensorDeviceClass.ENERGY,
219
  state_class=SensorStateClass.TOTAL,
220
  entity_registry_enabled_default=False,
221
  ),
222
  PlugwiseSensorEntityDescription(
223
+ key=EL_PROD_P_INTERVAL,
224
+ translation_key=EL_PROD_P_INTERVAL,
225
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
226
  device_class=SensorDeviceClass.ENERGY,
227
  state_class=SensorStateClass.TOTAL,
228
  ),
229
  PlugwiseSensorEntityDescription(
230
+ key=EL_PROD_OP_INTERVAL,
231
+ translation_key=EL_PROD_OP_INTERVAL,
232
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
233
  device_class=SensorDeviceClass.ENERGY,
234
  state_class=SensorStateClass.TOTAL,
235
  ),
236
  PlugwiseSensorEntityDescription(
237
+ key=EL_CONS_POINT,
238
+ translation_key=EL_CONS_POINT,
239
  device_class=SensorDeviceClass.POWER,
240
  native_unit_of_measurement=UnitOfPower.WATT,
241
  state_class=SensorStateClass.MEASUREMENT,
242
  ),
243
  PlugwiseSensorEntityDescription(
244
+ key=EL_CONS_OP_POINT,
245
+ translation_key=EL_CONS_OP_POINT,
246
  native_unit_of_measurement=UnitOfPower.WATT,
247
  device_class=SensorDeviceClass.POWER,
248
  state_class=SensorStateClass.MEASUREMENT,
249
  ),
250
  PlugwiseSensorEntityDescription(
251
+ key=EL_CONS_P_POINT,
252
+ translation_key=EL_CONS_P_POINT,
253
  native_unit_of_measurement=UnitOfPower.WATT,
254
  device_class=SensorDeviceClass.POWER,
255
  state_class=SensorStateClass.MEASUREMENT,
256
  ),
257
  PlugwiseSensorEntityDescription(
258
+ key=EL_CONS_OP_CUMULATIVE,
259
+ translation_key=EL_CONS_OP_CUMULATIVE,
260
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
261
  device_class=SensorDeviceClass.ENERGY,
262
  state_class=SensorStateClass.TOTAL_INCREASING,
263
  ),
264
  PlugwiseSensorEntityDescription(
265
+ key=EL_CONS_P_CUMULATIVE,
266
+ translation_key=EL_CONS_P_CUMULATIVE,
267
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
268
  device_class=SensorDeviceClass.ENERGY,
269
  state_class=SensorStateClass.TOTAL_INCREASING,
270
  ),
271
  PlugwiseSensorEntityDescription(
272
+ key=EL_PROD_POINT,
273
+ translation_key=EL_PROD_POINT,
274
  device_class=SensorDeviceClass.POWER,
275
  native_unit_of_measurement=UnitOfPower.WATT,
276
  state_class=SensorStateClass.MEASUREMENT,
277
  ),
278
  PlugwiseSensorEntityDescription(
279
+ key=EL_PROD_OP_POINT,
280
+ translation_key=EL_PROD_OP_POINT,
281
  native_unit_of_measurement=UnitOfPower.WATT,
282
  device_class=SensorDeviceClass.POWER,
283
  state_class=SensorStateClass.MEASUREMENT,
284
  ),
285
  PlugwiseSensorEntityDescription(
286
+ key=EL_PROD_P_POINT,
287
+ translation_key=EL_PROD_P_POINT,
288
  native_unit_of_measurement=UnitOfPower.WATT,
289
  device_class=SensorDeviceClass.POWER,
290
  state_class=SensorStateClass.MEASUREMENT,
291
  ),
292
  PlugwiseSensorEntityDescription(
293
+ key=EL_PROD_OP_CUMULATIVE,
294
+ translation_key=EL_PROD_OP_CUMULATIVE,
295
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
296
  device_class=SensorDeviceClass.ENERGY,
297
  state_class=SensorStateClass.TOTAL_INCREASING,
298
  ),
299
  PlugwiseSensorEntityDescription(
300
+ key=EL_PROD_P_CUMULATIVE,
301
+ translation_key=EL_PROD_P_CUMULATIVE,
302
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
303
  device_class=SensorDeviceClass.ENERGY,
304
  state_class=SensorStateClass.TOTAL_INCREASING,
305
  ),
306
  PlugwiseSensorEntityDescription(
307
+ key=EL_PH1_CONSUMED,
308
+ translation_key=EL_PH1_CONSUMED,
309
  device_class=SensorDeviceClass.POWER,
310
  native_unit_of_measurement=UnitOfPower.WATT,
311
  state_class=SensorStateClass.MEASUREMENT,
312
  ),
313
  PlugwiseSensorEntityDescription(
314
+ key=EL_PH2_CONSUMED,
315
+ translation_key=EL_PH2_CONSUMED,
316
  device_class=SensorDeviceClass.POWER,
317
  native_unit_of_measurement=UnitOfPower.WATT,
318
  state_class=SensorStateClass.MEASUREMENT,
319
  ),
320
  PlugwiseSensorEntityDescription(
321
+ key=EL_PH3_CONSUMED,
322
+ translation_key=EL_PH3_CONSUMED,
323
  device_class=SensorDeviceClass.POWER,
324
  native_unit_of_measurement=UnitOfPower.WATT,
325
  state_class=SensorStateClass.MEASUREMENT,
326
  ),
327
  PlugwiseSensorEntityDescription(
328
+ key=EL_PH1_PRODUCED,
329
+ translation_key=EL_PH1_PRODUCED,
330
  device_class=SensorDeviceClass.POWER,
331
  native_unit_of_measurement=UnitOfPower.WATT,
332
  state_class=SensorStateClass.MEASUREMENT,
333
  ),
334
  PlugwiseSensorEntityDescription(
335
+ key=EL_PH2_PRODUCED,
336
+ translation_key=EL_PH2_PRODUCED,
337
  device_class=SensorDeviceClass.POWER,
338
  native_unit_of_measurement=UnitOfPower.WATT,
339
  state_class=SensorStateClass.MEASUREMENT,
340
  ),
341
  PlugwiseSensorEntityDescription(
342
+ key=EL_PH3_PRODUCED,
343
+ translation_key=EL_PH3_PRODUCED,
344
  device_class=SensorDeviceClass.POWER,
345
  native_unit_of_measurement=UnitOfPower.WATT,
346
  state_class=SensorStateClass.MEASUREMENT,
347
  ),
348
  PlugwiseSensorEntityDescription(
349
+ key=VOLTAGE_PH1,
350
+ translation_key=VOLTAGE_PH1,
351
  device_class=SensorDeviceClass.VOLTAGE,
352
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
353
  state_class=SensorStateClass.MEASUREMENT,
354
  entity_registry_enabled_default=False,
355
  ),
356
  PlugwiseSensorEntityDescription(
357
+ key=VOLTAGE_PH2,
358
+ translation_key=VOLTAGE_PH2,
359
  device_class=SensorDeviceClass.VOLTAGE,
360
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
361
  state_class=SensorStateClass.MEASUREMENT,
362
  entity_registry_enabled_default=False,
363
  ),
364
  PlugwiseSensorEntityDescription(
365
+ key=VOLTAGE_PH3,
366
+ translation_key=VOLTAGE_PH3,
367
  device_class=SensorDeviceClass.VOLTAGE,
368
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
369
  state_class=SensorStateClass.MEASUREMENT,
370
  entity_registry_enabled_default=False,
371
  ),
372
  PlugwiseSensorEntityDescription(
373
+ key=GAS_CONS_INTERVAL,
374
+ translation_key=GAS_CONS_INTERVAL,
375
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
376
  state_class=SensorStateClass.MEASUREMENT,
377
  ),
378
  PlugwiseSensorEntityDescription(
379
+ key=GAS_CONS_CUMULATIVE,
380
+ translation_key=GAS_CONS_CUMULATIVE,
381
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
382
  device_class=SensorDeviceClass.GAS,
383
  state_class=SensorStateClass.TOTAL,
384
  ),
385
  PlugwiseSensorEntityDescription(
386
+ key=NET_EL_POINT,
387
+ translation_key=NET_EL_POINT,
388
  native_unit_of_measurement=UnitOfPower.WATT,
389
  device_class=SensorDeviceClass.POWER,
390
  state_class=SensorStateClass.MEASUREMENT,
391
  ),
392
  PlugwiseSensorEntityDescription(
393
+ key=NET_EL_CUMULATIVE,
394
+ translation_key=NET_EL_CUMULATIVE,
395
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
396
  device_class=SensorDeviceClass.ENERGY,
397
  state_class=SensorStateClass.TOTAL,
 
411
  entity_category=EntityCategory.DIAGNOSTIC,
412
  ),
413
  PlugwiseSensorEntityDescription(
414
+ key=MOD_LEVEL,
415
+ translation_key=MOD_LEVEL,
416
  native_unit_of_measurement=PERCENTAGE,
417
  entity_category=EntityCategory.DIAGNOSTIC,
418
  state_class=SensorStateClass.MEASUREMENT,
419
  ),
420
  PlugwiseSensorEntityDescription(
421
+ key=VALVE_POS,
422
+ translation_key=VALVE_POS,
423
  entity_category=EntityCategory.DIAGNOSTIC,
424
  native_unit_of_measurement=PERCENTAGE,
425
  state_class=SensorStateClass.MEASUREMENT,
426
  ),
427
  PlugwiseSensorEntityDescription(
428
+ key=WATER_PRESSURE,
429
+ translation_key=WATER_PRESSURE,
430
  native_unit_of_measurement=UnitOfPressure.BAR,
431
  device_class=SensorDeviceClass.PRESSURE,
432
  entity_category=EntityCategory.DIAGNOSTIC,
 
439
  state_class=SensorStateClass.MEASUREMENT,
440
  ),
441
  PlugwiseSensorEntityDescription(
442
+ key=DHW_TEMP,
443
+ translation_key=DHW_TEMP,
444
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
445
  device_class=SensorDeviceClass.TEMPERATURE,
446
  entity_category=EntityCategory.DIAGNOSTIC,
447
  state_class=SensorStateClass.MEASUREMENT,
448
  ),
449
  PlugwiseSensorEntityDescription(
450
+ key=DHW_SETPOINT,
451
+ translation_key=DHW_SETPOINT,
452
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
453
  device_class=SensorDeviceClass.TEMPERATURE,
454
  entity_category=EntityCategory.DIAGNOSTIC,
 
462
  entry: PlugwiseConfigEntry,
463
  async_add_entities: AddEntitiesCallback,
464
  ) -> None:
465
+ """Set up Plugwise sensors from a config entry."""
466
+ # Upstream as Plugwise not Smile
467
  coordinator = entry.runtime_data
468
 
469
  @callback
 
472
  if not coordinator.new_devices:
473
  return
474
 
475
+ # Upstream consts
476
+ # async_add_entities(
477
+ # PlugwiseSensorEntity(coordinator, device_id, description)
478
+ # for device_id in coordinator.new_devices
479
+ # if (sensors := coordinator.data.devices[device_id].get(SENSORS))
480
+ # for description in PLUGWISE_SENSORS
481
+ # if description.key in sensors
482
+ # )
483
+ # pw-beta alternative for debugging
484
+ entities: list[PlugwiseSensorEntity] = []
485
+ for device_id in coordinator.new_devices:
486
+ device = coordinator.data.devices[device_id]
487
+ if not (sensors := device.get(SENSORS)):
488
+ continue
489
+ for description in PLUGWISE_SENSORS:
490
+ if description.key not in sensors:
491
+ continue
492
+ entities.append(PlugwiseSensorEntity(coordinator, device_id, description))
493
+ LOGGER.debug(
494
+ "Add %s %s sensor", device["name"], description.translation_key or description.key
495
+ )
496
+
497
+ async_add_entities(entities)
498
 
499
  _add_entities()
500
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
513
  ) -> None:
514
  """Initialise the sensor."""
515
  super().__init__(coordinator, device_id)
 
516
  self.entity_description = description
517
+ self._attr_unique_id = f"{device_id}-{description.key}"
518
 
519
  @property
520
  def native_value(self) -> int | float:
521
  """Return the value reported by the sensor."""
522
+ return self.device[SENSORS][self.entity_description.key] # Upstream consts
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/strings.json RENAMED
@@ -1,47 +1,57 @@
1
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  "config": {
3
  "step": {
4
  "reconfigure": {
5
  "description": "Update configuration for {title}.",
6
  "data": {
7
- "host": "[%key:common::config_flow::data::ip%]",
8
- "port": "[%key:common::config_flow::data::port%]"
9
- },
10
- "data_description": {
11
- "host": "[%key:component::plugwise::config::step::user::data_description::host%]",
12
- "port": "[%key:component::plugwise::config::step::user::data_description::port%]"
13
  }
14
  },
15
  "user": {
16
- "title": "Connect to the Smile",
17
- "description": "Please enter",
18
  "data": {
19
- "host": "[%key:common::config_flow::data::ip%]",
20
- "password": "Smile ID",
21
- "port": "[%key:common::config_flow::data::port%]",
22
- "username": "Smile Username"
23
- },
24
- "data_description": {
25
- "password": "The Smile ID printed on the label on the back of your Adam, Smile-T, or P1.",
26
- "host": "The hostname or IP-address of your Smile. You can find it in your router or the Plugwise App.",
27
- "port": "By default your Smile uses port 80, normally you should not have to change this.",
28
- "username": "Default is `smile`, or `stretch` for the legacy Stretch."
29
  }
30
  }
31
  },
32
  "error": {
33
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
34
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
35
  "invalid_setup": "Add your Adam instead of your Anna, see the documentation",
 
 
36
  "response_error": "Invalid XML data, or error indication received",
37
- "unknown": "[%key:common::config_flow::error::unknown%]",
 
38
  "unsupported": "Device with unsupported firmware"
39
  },
40
  "abort": {
41
- "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
42
  "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
43
  "not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address.",
44
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
45
  }
46
  },
47
  "entity": {
@@ -59,10 +69,10 @@
59
  "name": "Flame state"
60
  },
61
  "heating_state": {
62
- "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]"
63
  },
64
  "cooling_state": {
65
- "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
66
  },
67
  "secondary_boiler_state": {
68
  "name": "Secondary boiler state"
@@ -79,20 +89,14 @@
79
  "climate": {
80
  "plugwise": {
81
  "state_attributes": {
82
- "available_schemas": {
83
- "name": "Available schemas"
84
- },
85
  "preset_mode": {
86
  "state": {
87
  "asleep": "Night",
88
- "away": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
89
- "home": "[%key:common::state::home%]",
90
  "no_frost": "Anti-frost",
91
  "vacation": "Vacation"
92
  }
93
- },
94
- "selected_schema": {
95
- "name": "Selected schema"
96
  }
97
  }
98
  }
@@ -112,10 +116,20 @@
112
  "dhw_mode": {
113
  "name": "DHW mode",
114
  "state": {
115
- "off": "[%key:common::state::off%]",
116
  "auto": "Auto",
117
- "boost": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::boost%]",
118
- "comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]"
 
 
 
 
 
 
 
 
 
 
 
119
  }
120
  },
121
  "gateway_mode": {
@@ -126,16 +140,6 @@
126
  "vacation": "Vacation"
127
  }
128
  },
129
- "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": {
@@ -177,6 +181,12 @@
177
  "electricity_produced": {
178
  "name": "Electricity produced"
179
  },
 
 
 
 
 
 
180
  "electricity_consumed_interval": {
181
  "name": "Electricity consumed interval"
182
  },
@@ -195,9 +205,6 @@
195
  "electricity_produced_off_peak_interval": {
196
  "name": "Electricity produced off peak interval"
197
  },
198
- "electricity_consumed_point": {
199
- "name": "Electricity consumed point"
200
- },
201
  "electricity_consumed_off_peak_point": {
202
  "name": "Electricity consumed off peak point"
203
  },
@@ -210,9 +217,6 @@
210
  "electricity_consumed_peak_cumulative": {
211
  "name": "Electricity consumed peak cumulative"
212
  },
213
- "electricity_produced_point": {
214
- "name": "Electricity produced point"
215
- },
216
  "electricity_produced_off_peak_point": {
217
  "name": "Electricity produced off peak point"
218
  },
@@ -280,18 +284,18 @@
280
  "name": "DHW setpoint"
281
  },
282
  "maximum_boiler_temperature": {
283
- "name": "Maximum boiler temperature"
284
  }
285
  },
286
  "switch": {
287
  "cooling_ena_switch": {
288
- "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
289
  },
290
  "dhw_cm_switch": {
291
  "name": "DHW comfort mode"
292
  },
293
  "lock": {
294
- "name": "[%key:component::lock::title%]"
295
  },
296
  "relay": {
297
  "name": "Relay"
@@ -300,7 +304,7 @@
300
  },
301
  "exceptions": {
302
  "authentication_failed": {
303
- "message": "[%key:common::config_flow::error::invalid_auth%]"
304
  },
305
  "data_incomplete_or_missing": {
306
  "message": "Data incomplete or missing."
@@ -309,16 +313,19 @@
309
  "message": "Error communicating with API: {error}."
310
  },
311
  "failed_to_connect": {
312
- "message": "[%key:common::config_flow::error::cannot_connect%]"
313
  },
314
  "invalid_xml_data": {
315
- "message": "[%key:component::plugwise::config::error::response_error%]"
316
  },
317
  "unsupported_firmware": {
318
- "message": "[%key:component::plugwise::config::error::unsupported%]"
319
- },
320
- "unsupported_hvac_mode_requested": {
321
- "message": "Unsupported mode {hvac_mode} requested, valid modes are: {hvac_modes}."
 
 
 
322
  }
323
  }
324
  }
 
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
  "reconfigure": {
22
  "description": "Update configuration for {title}.",
23
  "data": {
24
+ "host": "IP-address",
25
+ "port": "Port number"
 
 
 
 
26
  }
27
  },
28
  "user": {
29
+ "title": "Set up Plugwise Adam/Smile/Stretch",
30
+ "description": "Enter your Plugwise device: (setup can take up to 90s)",
31
  "data": {
32
+ "password": "ID",
33
+ "username": "Username",
34
+ "host": "IP-address",
35
+ "port": "Port number"
 
 
 
 
 
 
36
  }
37
  }
38
  },
39
  "error": {
40
+ "cannot_connect": "Failed to connect",
41
+ "invalid_auth": "Authentication failed",
42
  "invalid_setup": "Add your Adam instead of your Anna, see the documentation",
43
+ "network_down": "Plugwise Zigbee network is down",
44
+ "network_timeout": "Network communication timeout",
45
  "response_error": "Invalid XML data, or error indication received",
46
+ "stick_init": "Initialization of Plugwise USB-stick failed",
47
+ "unknown": "Unknown error!",
48
  "unsupported": "Device with unsupported firmware"
49
  },
50
  "abort": {
51
+ "already_configured": "This device is already configured",
52
  "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
53
  "not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address.",
54
+ "reconfigure_successful": "Reconfiguration successful"
55
  }
56
  },
57
  "entity": {
 
69
  "name": "Flame state"
70
  },
71
  "heating_state": {
72
+ "name": "Heating"
73
  },
74
  "cooling_state": {
75
+ "name": "Cooling"
76
  },
77
  "secondary_boiler_state": {
78
  "name": "Secondary boiler state"
 
89
  "climate": {
90
  "plugwise": {
91
  "state_attributes": {
 
 
 
92
  "preset_mode": {
93
  "state": {
94
  "asleep": "Night",
95
+ "away": "Away",
96
+ "home": "Home",
97
  "no_frost": "Anti-frost",
98
  "vacation": "Vacation"
99
  }
 
 
 
100
  }
101
  }
102
  }
 
116
  "dhw_mode": {
117
  "name": "DHW mode",
118
  "state": {
 
119
  "auto": "Auto",
120
+ "boost": "Boost",
121
+ "comfort": "Comfort",
122
+ "off": "Off"
123
+ }
124
+ },
125
+ "regulation_mode": {
126
+ "name": "Regulation mode",
127
+ "state": {
128
+ "bleeding_cold": "Bleeding cold",
129
+ "bleeding_hot": "Bleeding hot",
130
+ "cooling": "Cooling",
131
+ "heating": "Heating",
132
+ "off": "Off"
133
  }
134
  },
135
  "gateway_mode": {
 
140
  "vacation": "Vacation"
141
  }
142
  },
 
 
 
 
 
 
 
 
 
 
143
  "select_schedule": {
144
  "name": "Thermostat schedule",
145
  "state": {
 
181
  "electricity_produced": {
182
  "name": "Electricity produced"
183
  },
184
+ "electricity_consumed_point": {
185
+ "name": "Electricity consumed point"
186
+ },
187
+ "electricity_produced_point": {
188
+ "name": "Electricity produced point"
189
+ },
190
  "electricity_consumed_interval": {
191
  "name": "Electricity consumed interval"
192
  },
 
205
  "electricity_produced_off_peak_interval": {
206
  "name": "Electricity produced off peak interval"
207
  },
 
 
 
208
  "electricity_consumed_off_peak_point": {
209
  "name": "Electricity consumed off peak point"
210
  },
 
217
  "electricity_consumed_peak_cumulative": {
218
  "name": "Electricity consumed peak cumulative"
219
  },
 
 
 
220
  "electricity_produced_off_peak_point": {
221
  "name": "Electricity produced off peak point"
222
  },
 
284
  "name": "DHW setpoint"
285
  },
286
  "maximum_boiler_temperature": {
287
+ "name": "Maximum boiler temperature setpoint"
288
  }
289
  },
290
  "switch": {
291
  "cooling_ena_switch": {
292
+ "name": "Cooling"
293
  },
294
  "dhw_cm_switch": {
295
  "name": "DHW comfort mode"
296
  },
297
  "lock": {
298
+ "name": "Lock"
299
  },
300
  "relay": {
301
  "name": "Relay"
 
304
  },
305
  "exceptions": {
306
  "authentication_failed": {
307
+ "message": "Invalid authentication"
308
  },
309
  "data_incomplete_or_missing": {
310
  "message": "Data incomplete or missing."
 
313
  "message": "Error communicating with API: {error}."
314
  },
315
  "failed_to_connect": {
316
+ "message": "Failed to connect"
317
  },
318
  "invalid_xml_data": {
319
+ "message": "Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch"
320
  },
321
  "unsupported_firmware": {
322
+ "message": "Device with unsupported firmware"
323
+ }
324
+ },
325
+ "services": {
326
+ "delete_notification": {
327
+ "name": "Delete Plugwise notification",
328
+ "description": "Deletes a Plugwise Notification"
329
  }
330
  }
331
  }
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/switch.py RENAMED
@@ -17,6 +17,17 @@
17
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
18
 
19
  from . import PlugwiseConfigEntry
 
 
 
 
 
 
 
 
 
 
 
20
  from .coordinator import PlugwiseDataUpdateCoordinator
21
  from .entity import PlugwiseEntity
22
  from .util import plugwise_command
@@ -31,25 +42,29 @@
31
  key: SwitchType
32
 
33
 
34
- SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
 
35
  PlugwiseSwitchEntityDescription(
36
- key="dhw_cm_switch",
37
- translation_key="dhw_cm_switch",
 
38
  entity_category=EntityCategory.CONFIG,
39
  ),
40
  PlugwiseSwitchEntityDescription(
41
- key="lock",
42
- translation_key="lock",
 
43
  entity_category=EntityCategory.CONFIG,
44
  ),
45
  PlugwiseSwitchEntityDescription(
46
- key="relay",
47
- translation_key="relay",
48
  device_class=SwitchDeviceClass.SWITCH,
49
  ),
50
  PlugwiseSwitchEntityDescription(
51
- key="cooling_ena_switch",
52
- translation_key="cooling_ena_switch",
 
53
  entity_category=EntityCategory.CONFIG,
54
  ),
55
  )
@@ -60,7 +75,7 @@
60
  entry: PlugwiseConfigEntry,
61
  async_add_entities: AddEntitiesCallback,
62
  ) -> None:
63
- """Set up the Smile switches from a config entry."""
64
  coordinator = entry.runtime_data
65
 
66
  @callback
@@ -69,13 +84,28 @@
69
  if not coordinator.new_devices:
70
  return
71
 
72
- async_add_entities(
73
- PlugwiseSwitchEntity(coordinator, device_id, description)
74
- for device_id in coordinator.new_devices
75
- if (switches := coordinator.data.devices[device_id].get("switches"))
76
- for description in SWITCHES
77
- if description.key in switches
78
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  _add_entities()
81
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -94,30 +124,30 @@
94
  ) -> None:
95
  """Set up the Plugwise API."""
96
  super().__init__(coordinator, device_id)
97
- self._attr_unique_id = f"{device_id}-{description.key}"
98
  self.entity_description = description
 
99
 
100
  @property
101
  def is_on(self) -> bool:
102
  """Return True if entity is on."""
103
- return self.device["switches"][self.entity_description.key]
104
 
105
  @plugwise_command
106
  async def async_turn_on(self, **kwargs: Any) -> None:
107
  """Turn the device on."""
108
  await self.coordinator.api.set_switch_state(
109
  self._dev_id,
110
- self.device.get("members"),
111
  self.entity_description.key,
112
  "on",
113
- )
114
 
115
  @plugwise_command
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
- )
 
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
 
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
  )
 
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
  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
  ) -> 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
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/util.py RENAMED
@@ -1,5 +1,7 @@
1
  """Utilities for Plugwise."""
2
 
 
 
3
  from collections.abc import Awaitable, Callable, Coroutine
4
  from typing import Any, Concatenate
5
 
@@ -10,6 +12,11 @@
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]],
 
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
 
 
12
  from .const import DOMAIN
13
  from .entity import PlugwiseEntity
14
 
15
+ # For reference:
16
+ # _PlugwiseEntityT = TypeVar("_PlugwiseEntityT", bound=PlugwiseEntity)
17
+ # _R = TypeVar("_R")
18
+ # _P = ParamSpec("_P")
19
+
20
 
21
  def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
22
  func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],