Diff to HTML by rtfpessoa

Files changed (15) hide show
  1. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/__init__.py +77 -21
  2. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/binary_sensor.py +94 -35
  3. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/button.py +17 -7
  4. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/climate.py +127 -89
  5. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/config_flow.py +132 -28
  6. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/const.py +154 -33
  7. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/coordinator.py +48 -31
  8. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/entity.py +24 -10
  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 +4 -4
  11. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/number.py +52 -29
  12. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/select.py +46 -17
  13. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/sensor.py +172 -101
  14. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/switch.py +50 -27
  15. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/util.py +10 -5
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/__init__.py RENAMED
@@ -2,21 +2,46 @@
2
 
3
  from typing import Any
4
 
5
- from homeassistant.const import Platform
6
  from homeassistant.core import HomeAssistant, callback
7
  from homeassistant.helpers import device_registry as dr, entity_registry as er
 
8
 
9
- from .const import DEV_CLASS, DOMAIN, LOGGER, PLATFORMS
 
 
 
 
 
10
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
 
 
 
 
11
 
12
 
13
  async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
14
- """Set up Plugwise components from a config entry."""
15
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
16
 
17
- coordinator = PlugwiseDataUpdateCoordinator(hass, entry)
 
 
 
 
 
 
 
 
 
18
  await coordinator.async_config_entry_first_refresh()
19
- migrate_sensor_entities(hass, coordinator)
 
20
 
21
  entry.runtime_data = coordinator
22
 
@@ -33,20 +58,25 @@
33
 
34
  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
35
 
 
 
36
  return True
37
 
 
 
 
 
 
38
 
39
  async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
40
- """Unload the Plugwise components."""
41
  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
42
 
43
-
44
  @callback
45
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
46
  """Migrate Plugwise entity entries.
47
 
48
- Migrates old unique ID's from old binary_sensors and
49
- switches to the new unique ID's.
50
  """
51
  if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
52
  "-slave_boiler_state"
@@ -68,29 +98,55 @@
68
  # No migration needed
69
  return None
70
 
71
-
72
- def migrate_sensor_entities(
73
  hass: HomeAssistant,
74
  coordinator: PlugwiseDataUpdateCoordinator,
75
  ) -> None:
76
- """Migrate Sensors if needed."""
77
  ent_reg = er.async_get(hass)
78
 
79
- # Migrating opentherm_outdoor_temperature
80
- # to opentherm_outdoor_air_temperature sensor
81
  for device_id, device in coordinator.data.items():
82
- if device[DEV_CLASS] != "heater_central":
83
  continue
84
 
 
 
85
  old_unique_id = f"{device_id}-outdoor_temperature"
86
  if entity_id := ent_reg.async_get_entity_id(
87
  Platform.SENSOR, DOMAIN, old_unique_id
88
  ):
89
  new_unique_id = f"{device_id}-outdoor_air_temperature"
90
- LOGGER.debug(
91
- "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
92
- entity_id,
93
- old_unique_id,
94
- new_unique_id,
95
- )
 
 
 
 
96
  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  from typing import Any
4
 
5
+ from homeassistant.const import ATTR_NAME, CONF_TIMEOUT, Platform
6
  from homeassistant.core import HomeAssistant, callback
7
  from homeassistant.helpers import device_registry as dr, entity_registry as er
8
+ from homeassistant.helpers.typing import ConfigType
9
 
10
+ from .const import (
11
+ CONF_REFRESH_INTERVAL, # pw-beta options
12
+ DOMAIN,
13
+ LOGGER,
14
+ PLATFORMS,
15
+ )
16
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
17
+ from .services import async_setup_services
18
+
19
+
20
+ async def async_setup(hass: HomeAssistant, _config: ConfigType) -> bool:
21
+ """Set up my integration."""
22
+
23
+ async_setup_services(hass)
24
+
25
+ return True
26
 
27
 
28
  async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
29
+ """Set up Plugwise from a config entry."""
30
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
31
 
32
+ cooldown = 1.5 # pw-beta frontend refresh-interval
33
+ if (
34
+ custom_refresh := entry.options.get(CONF_REFRESH_INTERVAL)
35
+ ) is not None: # pragma: no cover
36
+ cooldown = custom_refresh
37
+ LOGGER.debug("DUC cooldown interval: %s", cooldown)
38
+
39
+ coordinator = PlugwiseDataUpdateCoordinator(
40
+ hass, cooldown, entry
41
+ ) # pw-beta - cooldown, update_interval as extra
42
  await coordinator.async_config_entry_first_refresh()
43
+
44
+ await async_migrate_entities(hass, coordinator)
45
 
46
  entry.runtime_data = coordinator
47
 
 
58
 
59
  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
60
 
61
+ entry.async_on_unload(entry.add_update_listener(update_listener)) # pw-beta options_flow
62
+
63
  return True
64
 
65
+ async def update_listener(
66
+ hass: HomeAssistant, entry: PlugwiseConfigEntry
67
+ ) -> None: # pragma: no cover # pw-beta
68
+ """Handle options update."""
69
+ await hass.config_entries.async_reload(entry.entry_id)
70
 
71
  async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
72
+ """Unload Plugwise."""
73
  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
74
 
 
75
  @callback
76
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
77
  """Migrate Plugwise entity entries.
78
 
79
+ - Migrates old unique ID's from old binary_sensors and switches to the new unique ID's
 
80
  """
81
  if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
82
  "-slave_boiler_state"
 
98
  # No migration needed
99
  return None
100
 
101
+ async def async_migrate_entities(
 
102
  hass: HomeAssistant,
103
  coordinator: PlugwiseDataUpdateCoordinator,
104
  ) -> None:
105
+ """Migrate entities if needed."""
106
  ent_reg = er.async_get(hass)
107
 
 
 
108
  for device_id, device in coordinator.data.items():
109
+ if device["dev_class"] not in ("climate", "heater_central"):
110
  continue
111
 
112
+ # Migrate opentherm_outdoor_temperature
113
+ # to opentherm_outdoor_air_temperature sensor
114
  old_unique_id = f"{device_id}-outdoor_temperature"
115
  if entity_id := ent_reg.async_get_entity_id(
116
  Platform.SENSOR, DOMAIN, old_unique_id
117
  ):
118
  new_unique_id = f"{device_id}-outdoor_air_temperature"
119
+ # Upstream remove LOGGER debug
120
+ ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
121
+
122
+ # Migrate id-climate to id-device-name
123
+ old_unique_id = f"{device_id}-climate"
124
+ if entity_id := ent_reg.async_get_entity_id(
125
+ Platform.CLIMATE, DOMAIN, old_unique_id
126
+ ):
127
+ new_unique_id = f"{device_id}-{device[ATTR_NAME]}".lower()
128
+ # Upstream remove LOGGER debug
129
  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
130
+
131
+
132
+ # pw-beta only - revert adding CONF_TIMEOUT to config_entry in v0.53.3
133
+ async def async_migrate_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
134
+ """Migrate back to v1.1 config entry."""
135
+ if entry.version > 1:
136
+ # This means the user has downgraded from a future version
137
+ return False #pragma: no cover
138
+
139
+ if entry.version == 1 and entry.minor_version == 2:
140
+ new_data = {**entry.data}
141
+ new_data.pop(CONF_TIMEOUT)
142
+ hass.config_entries.async_update_entry(
143
+ entry, data=new_data, minor_version=1, version=1
144
+ )
145
+
146
+ LOGGER.debug(
147
+ "Migration to version %s.%s successful",
148
+ entry.version,
149
+ entry.minor_version,
150
+ )
151
+
152
+ return True
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/binary_sensor.py RENAMED
@@ -6,6 +6,9 @@
6
 
7
  from plugwise.constants import BinarySensorType
8
 
 
 
 
9
  from homeassistant.components.binary_sensor import (
10
  BinarySensorDeviceClass,
11
  BinarySensorEntity,
@@ -15,11 +18,26 @@
15
  from homeassistant.core import HomeAssistant, callback
16
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
19
  from .entity import PlugwiseEntity
20
 
21
- SEVERITIES = ["other", "info", "warning", "error"]
22
-
23
  # Coordinator is used to centralize the data updates
24
  PARALLEL_UPDATES = 0
25
 
@@ -31,50 +49,51 @@
31
  key: BinarySensorType
32
 
33
 
34
- BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
 
35
  PlugwiseBinarySensorEntityDescription(
36
- key="low_battery",
37
  device_class=BinarySensorDeviceClass.BATTERY,
38
  entity_category=EntityCategory.DIAGNOSTIC,
39
  ),
40
  PlugwiseBinarySensorEntityDescription(
41
- key="compressor_state",
42
- translation_key="compressor_state",
43
  entity_category=EntityCategory.DIAGNOSTIC,
44
  ),
45
  PlugwiseBinarySensorEntityDescription(
46
- key="cooling_enabled",
47
- translation_key="cooling_enabled",
48
  entity_category=EntityCategory.DIAGNOSTIC,
49
  ),
50
  PlugwiseBinarySensorEntityDescription(
51
- key="dhw_state",
52
- translation_key="dhw_state",
53
  entity_category=EntityCategory.DIAGNOSTIC,
54
  ),
55
  PlugwiseBinarySensorEntityDescription(
56
- key="flame_state",
57
- translation_key="flame_state",
58
  entity_category=EntityCategory.DIAGNOSTIC,
59
  ),
60
  PlugwiseBinarySensorEntityDescription(
61
- key="heating_state",
62
- translation_key="heating_state",
63
  entity_category=EntityCategory.DIAGNOSTIC,
64
  ),
65
  PlugwiseBinarySensorEntityDescription(
66
- key="cooling_state",
67
- translation_key="cooling_state",
68
  entity_category=EntityCategory.DIAGNOSTIC,
69
  ),
70
  PlugwiseBinarySensorEntityDescription(
71
- key="secondary_boiler_state",
72
- translation_key="secondary_boiler_state",
73
  entity_category=EntityCategory.DIAGNOSTIC,
74
  ),
75
  PlugwiseBinarySensorEntityDescription(
76
- key="plugwise_notification",
77
- translation_key="plugwise_notification",
78
  entity_category=EntityCategory.DIAGNOSTIC,
79
  ),
80
  )
@@ -85,7 +104,7 @@
85
  entry: PlugwiseConfigEntry,
86
  async_add_entities: AddConfigEntryEntitiesCallback,
87
  ) -> None:
88
- """Set up the Smile binary_sensors from a config entry."""
89
  coordinator = entry.runtime_data
90
 
91
  @callback
@@ -94,20 +113,40 @@
94
  if not coordinator.new_devices:
95
  return
96
 
97
- async_add_entities(
98
- PlugwiseBinarySensorEntity(coordinator, device_id, description)
99
- for device_id in coordinator.new_devices
100
- if (binary_sensors := coordinator.data[device_id].get("binary_sensors"))
101
- for description in BINARY_SENSORS
102
- if description.key in binary_sensors
103
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  _add_entities()
106
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
107
 
108
 
109
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
110
- """Represent Smile Binary Sensors."""
111
 
112
  entity_description: PlugwiseBinarySensorEntityDescription
113
 
@@ -121,28 +160,48 @@
121
  super().__init__(coordinator, device_id)
122
  self.entity_description = description
123
  self._attr_unique_id = f"{device_id}-{description.key}"
 
124
 
125
  @property
126
  @override
127
- def is_on(self) -> bool:
128
  """Return true if the binary sensor is on."""
129
- return self.device["binary_sensors"][self.entity_description.key]
 
 
 
 
 
 
 
130
 
131
  @property
132
  @override
133
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
134
  """Return entity specific state attributes."""
135
- if self.entity_description.key != "plugwise_notification":
136
  return None
137
 
138
- attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES}
 
 
 
139
  gateway_id = self.coordinator.api.gateway_id
140
  if notify := self.coordinator.data[gateway_id]["notifications"]:
141
- for details in notify.values():
142
  for msg_type, msg in details.items():
143
  msg_type = msg_type.lower()
144
  if msg_type not in SEVERITIES:
145
- msg_type = "other"
 
 
 
 
 
146
  attrs[f"{msg_type}_msg"].append(msg)
147
 
 
 
 
 
148
  return attrs
 
6
 
7
  from plugwise.constants import BinarySensorType
8
 
9
+ from homeassistant.components import (
10
+ persistent_notification, # pw-beta Plugwise notifications
11
+ )
12
  from homeassistant.components.binary_sensor import (
13
  BinarySensorDeviceClass,
14
  BinarySensorEntity,
 
18
  from homeassistant.core import HomeAssistant, callback
19
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
20
 
21
+ from .const import (
22
+ BATTERY_STATE,
23
+ BINARY_SENSORS,
24
+ COMPRESSOR_STATE,
25
+ COOLING_ENABLED,
26
+ COOLING_STATE,
27
+ DHW_STATE,
28
+ DOMAIN,
29
+ FLAME_STATE,
30
+ HEATING_STATE,
31
+ LOGGER, # pw-beta
32
+ PLUGWISE_NOTIFICATION,
33
+ SECONDARY_BOILER_STATE,
34
+ SEVERITIES,
35
+ )
36
+
37
+ # Upstream
38
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
39
  from .entity import PlugwiseEntity
40
 
 
 
41
  # Coordinator is used to centralize the data updates
42
  PARALLEL_UPDATES = 0
43
 
 
49
  key: BinarySensorType
50
 
51
 
52
+ # Upstream PLUGWISE_BINARY_SENSORS
53
+ PLUGWISE_BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
54
  PlugwiseBinarySensorEntityDescription(
55
+ key=BATTERY_STATE,
56
  device_class=BinarySensorDeviceClass.BATTERY,
57
  entity_category=EntityCategory.DIAGNOSTIC,
58
  ),
59
  PlugwiseBinarySensorEntityDescription(
60
+ key=COMPRESSOR_STATE,
61
+ translation_key=COMPRESSOR_STATE,
62
  entity_category=EntityCategory.DIAGNOSTIC,
63
  ),
64
  PlugwiseBinarySensorEntityDescription(
65
+ key=COOLING_ENABLED,
66
+ translation_key=COOLING_ENABLED,
67
  entity_category=EntityCategory.DIAGNOSTIC,
68
  ),
69
  PlugwiseBinarySensorEntityDescription(
70
+ key=DHW_STATE,
71
+ translation_key=DHW_STATE,
72
  entity_category=EntityCategory.DIAGNOSTIC,
73
  ),
74
  PlugwiseBinarySensorEntityDescription(
75
+ key=FLAME_STATE,
76
+ translation_key=FLAME_STATE,
77
  entity_category=EntityCategory.DIAGNOSTIC,
78
  ),
79
  PlugwiseBinarySensorEntityDescription(
80
+ key=HEATING_STATE,
81
+ translation_key=HEATING_STATE,
82
  entity_category=EntityCategory.DIAGNOSTIC,
83
  ),
84
  PlugwiseBinarySensorEntityDescription(
85
+ key=COOLING_STATE,
86
+ translation_key=COOLING_STATE,
87
  entity_category=EntityCategory.DIAGNOSTIC,
88
  ),
89
  PlugwiseBinarySensorEntityDescription(
90
+ key=SECONDARY_BOILER_STATE,
91
+ translation_key=SECONDARY_BOILER_STATE,
92
  entity_category=EntityCategory.DIAGNOSTIC,
93
  ),
94
  PlugwiseBinarySensorEntityDescription(
95
+ key=PLUGWISE_NOTIFICATION,
96
+ translation_key=PLUGWISE_NOTIFICATION,
97
  entity_category=EntityCategory.DIAGNOSTIC,
98
  ),
99
  )
 
104
  entry: PlugwiseConfigEntry,
105
  async_add_entities: AddConfigEntryEntitiesCallback,
106
  ) -> None:
107
+ """Set up Plugwise binary_sensors from a config entry."""
108
  coordinator = entry.runtime_data
109
 
110
  @callback
 
113
  if not coordinator.new_devices:
114
  return
115
 
116
+ # Upstream consts to HA
117
+ # async_add_entities(
118
+ # PlugwiseBinarySensorEntity(coordinator, device_id, description)
119
+ # for device_id in coordinator.new_devices
120
+ # if (
121
+ # binary_sensors := coordinator.data.devices[device_id].get(
122
+ # BINARY_SENSORS
123
+ # )
124
+ # )
125
+ # for description in PLUGWISE_BINARY_SENSORS
126
+ # if description.key in binary_sensors
127
+ # )
128
+
129
+ # pw-beta alternative for debugging
130
+ entities: list[PlugwiseBinarySensorEntity] = []
131
+ for device_id in coordinator.new_devices:
132
+ device = coordinator.data[device_id]
133
+ if not (binary_sensors := device.get(BINARY_SENSORS)):
134
+ continue
135
+ for description in PLUGWISE_BINARY_SENSORS:
136
+ if description.key not in binary_sensors:
137
+ continue
138
+ entities.append(PlugwiseBinarySensorEntity(coordinator, device_id, description))
139
+ LOGGER.debug(
140
+ "Add %s %s binary sensor", device["name"], description.translation_key or description.key
141
+ )
142
+ async_add_entities(entities)
143
 
144
  _add_entities()
145
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
146
 
147
 
148
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
149
+ """Set up Plugwise binary_sensors from a config entry."""
150
 
151
  entity_description: PlugwiseBinarySensorEntityDescription
152
 
 
160
  super().__init__(coordinator, device_id)
161
  self.entity_description = description
162
  self._attr_unique_id = f"{device_id}-{description.key}"
163
+ self._notification: dict[str, str] = {} # pw-beta
164
 
165
  @property
166
  @override
167
+ def is_on(self) -> bool | None:
168
  """Return true if the binary sensor is on."""
169
+ # pw-beta: show Plugwise notifications as HA persistent notifications
170
+ if self._notification:
171
+ for notify_id, message in self._notification.items():
172
+ persistent_notification.async_create(
173
+ self.hass, message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
174
+ )
175
+
176
+ return self.device.get(BINARY_SENSORS, {}).get(self.entity_description.key)
177
 
178
  @property
179
  @override
180
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
181
  """Return entity specific state attributes."""
182
+ if self.entity_description.key != PLUGWISE_NOTIFICATION: # Upstream const
183
  return None
184
 
185
+ # pw-beta adjustment with attrs is to only represent severities *with* content
186
+ # not all severities including those without content as empty lists
187
+ attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
188
+ self._notification = {} # pw-beta
189
  gateway_id = self.coordinator.api.gateway_id
190
  if notify := self.coordinator.data[gateway_id]["notifications"]:
191
+ for notify_id, details in notify.items(): # pw-beta uses notify_id
192
  for msg_type, msg in details.items():
193
  msg_type = msg_type.lower()
194
  if msg_type not in SEVERITIES:
195
+ msg_type = "other" # pragma: no cover
196
+
197
+ if (
198
+ f"{msg_type}_msg" not in attrs
199
+ ): # pw-beta Re-evaluate against Core
200
+ attrs[f"{msg_type}_msg"] = []
201
  attrs[f"{msg_type}_msg"].append(msg)
202
 
203
+ self._notification[
204
+ notify_id
205
+ ] = f"{msg_type.title()}: {msg}" # pw-beta
206
+
207
  return attrs
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/button.py RENAMED
@@ -7,7 +7,10 @@
7
  from homeassistant.core import HomeAssistant
8
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
9
 
10
- from .const import REBOOT
 
 
 
11
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
12
  from .entity import PlugwiseEntity
13
  from .util import plugwise_command
@@ -20,14 +23,21 @@
20
  entry: PlugwiseConfigEntry,
21
  async_add_entities: AddConfigEntryEntitiesCallback,
22
  ) -> None:
23
- """Set up the Plugwise buttons from a ConfigEntry."""
24
  coordinator = entry.runtime_data
25
 
26
- async_add_entities(
27
- PlugwiseButtonEntity(coordinator, device_id)
28
- for device_id in coordinator.data
29
- if device_id == coordinator.api.gateway_id and coordinator.api.reboot
30
- )
 
 
 
 
 
 
 
31
 
32
 
33
  class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
 
7
  from homeassistant.core import HomeAssistant
8
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
9
 
10
+ from .const import (
11
+ LOGGER, # pw-betea
12
+ REBOOT,
13
+ )
14
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
15
  from .entity import PlugwiseEntity
16
  from .util import plugwise_command
 
23
  entry: PlugwiseConfigEntry,
24
  async_add_entities: AddConfigEntryEntitiesCallback,
25
  ) -> None:
26
+ """Set up Plugwise buttons from a config entry."""
27
  coordinator = entry.runtime_data
28
 
29
+ # async_add_entities(
30
+ # PlugwiseButtonEntity(coordinator, device_id)
31
+ # for device_id in coordinator.data.devices
32
+ # if device_id == gateway[GATEWAY_ID] and REBOOT in gateway
33
+ # )
34
+ # pw-beta alternative for debugging
35
+ entities: list[PlugwiseButtonEntity] = []
36
+ for device_id, device in coordinator.data.items():
37
+ if device_id == coordinator.api.gateway_id and coordinator.api.reboot:
38
+ entities.append(PlugwiseButtonEntity(coordinator, device_id))
39
+ LOGGER.debug("Add %s reboot button", device["name"])
40
+ async_add_entities(entities)
41
 
42
 
43
  class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/climate.py RENAMED
@@ -12,13 +12,39 @@
12
  HVACAction,
13
  HVACMode,
14
  )
15
- from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON, UnitOfTemperature
 
 
 
 
 
 
16
  from homeassistant.core import HomeAssistant, callback
17
  from homeassistant.exceptions import HomeAssistantError
18
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
19
  from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
20
 
21
- from .const import DOMAIN, MASTER_THERMOSTATS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
23
  from .entity import PlugwiseEntity
24
  from .util import plugwise_command
@@ -27,6 +53,42 @@
27
  PARALLEL_UPDATES = 0
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def _check_for_schedule(active: bool, last_active: str | None) -> None:
31
  """Raise a HAError when no thermostat schedule has been set."""
32
  if not active and last_active is None:
@@ -48,52 +110,15 @@
48
  """Return a dict representation of the text data."""
49
  return asdict(self)
50
 
51
- @classmethod
52
- def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData:
53
- """Initialize a stored data object from a dict."""
54
- return cls(
55
- last_active_schedule=restored.get("last_active_schedule"),
56
- previous_action_mode=restored.get("previous_action_mode"),
57
- )
58
-
59
-
60
- async def async_setup_entry(
61
- hass: HomeAssistant,
62
- entry: PlugwiseConfigEntry,
63
- async_add_entities: AddConfigEntryEntitiesCallback,
64
- ) -> None:
65
- """Set up the Smile Thermostats from a config entry."""
66
- coordinator = entry.runtime_data
67
-
68
- @callback
69
- def _add_entities() -> None:
70
- """Add Entities."""
71
- if not coordinator.new_devices:
72
- return
73
-
74
- if coordinator.api.smile.name == "Adam":
75
- async_add_entities(
76
- PlugwiseClimateEntity(coordinator, device_id)
77
- for device_id in coordinator.new_devices
78
- if coordinator.data[device_id]["dev_class"] == "climate"
79
- )
80
- else:
81
- async_add_entities(
82
- PlugwiseClimateEntity(coordinator, device_id)
83
- for device_id in coordinator.new_devices
84
- if coordinator.data[device_id]["dev_class"] in MASTER_THERMOSTATS
85
- )
86
-
87
- _add_entities()
88
- entry.async_on_unload(coordinator.async_add_listener(_add_entities))
89
-
90
 
91
  class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
92
  """Representation of a Plugwise thermostat."""
93
 
 
94
  _attr_name = None
95
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
96
  _attr_translation_key = DOMAIN
 
97
 
98
  def __init__(
99
  self,
@@ -102,20 +127,31 @@
102
  ) -> None:
103
  """Set up the Plugwise API."""
104
  super().__init__(coordinator, device_id)
105
- self._attr_unique_id = f"{device_id}-climate" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
106
 
107
  self._api = coordinator.api
108
  gateway_id: str = self._api.gateway_id
109
  self._gateway_data = coordinator.data[gateway_id]
110
  self._last_active_schedule: str | None = None
111
  self._location = device_id
112
- if (location := self.device.get("location")) is not None:
113
  self._location = location
114
  self._previous_action_mode = HVACAction.HEATING.value
115
 
 
 
 
 
 
 
 
 
 
116
  # Determine supported features
117
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
118
- if self._api.cooling_present and self._api.smile.name != "Adam":
 
 
 
119
  self._attr_supported_features = (
120
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
121
  )
@@ -123,100 +159,99 @@
123
  self._attr_supported_features |= (
124
  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
125
  )
126
- if presets := self.device.get("preset_modes"):
127
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
128
- self._attr_preset_modes = presets
129
-
130
- self._attr_min_temp = self.device["thermostat"]["lower_bound"]
131
- self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
132
- # Ensure we don't drop below 0.1
133
- self._attr_target_temperature_step = max(
134
- self.device["thermostat"]["resolution"], 0.1
135
- )
136
 
137
  @override
138
  async def async_added_to_hass(self) -> None:
139
  """Run when entity about to be added."""
140
- await super().async_added_to_hass()
141
 
142
- if extra_data := await self.async_get_last_extra_data():
143
- plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
144
- extra_data.as_dict()
145
- )
146
- self._last_active_schedule = plugwise_extra_data.last_active_schedule
147
  self._previous_action_mode = (
148
- plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value
149
  )
150
 
 
 
151
  @property
152
  @override
153
  def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
154
  """Return text specific state data to be restored."""
155
  return PlugwiseClimateExtraStoredData(
156
- last_active_schedule=self._last_active_schedule,
157
- previous_action_mode=self._previous_action_mode,
158
  )
159
 
160
  @property
161
  @override
162
  def current_temperature(self) -> float | None:
163
  """Return the current temperature."""
164
- return self.device["sensors"].get("temperature")
165
 
166
  @property
167
  @override
168
- def target_temperature(self) -> float:
169
  """Return the temperature we try to reach.
170
 
171
  Connected to the HVACMode combination of AUTO-HEAT.
172
  """
173
 
174
- return self.device["thermostat"]["setpoint"]
175
 
176
  @property
177
  @override
178
- def target_temperature_high(self) -> float:
179
  """Return the temperature we try to reach in case of cooling.
180
 
181
  Connected to the HVACMode combination of AUTO-HEAT_COOL.
182
  """
183
- return self.device["thermostat"]["setpoint_high"]
184
 
185
  @property
186
  @override
187
- def target_temperature_low(self) -> float:
188
  """Return the heating temperature we try to reach in case of heating.
189
 
190
  Connected to the HVACMode combination AUTO-HEAT_COOL.
191
  """
192
- return self.device["thermostat"]["setpoint_low"]
193
 
194
  @property
195
  @override
196
  def hvac_mode(self) -> HVACMode:
197
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
198
- if (
199
- mode := self.device.get("climate_mode")
200
- ) is None or mode not in self.hvac_modes:
201
- return HVACMode.HEAT
202
- return HVACMode(mode)
 
 
 
 
 
 
203
 
204
  @property
205
  @override
206
  def hvac_modes(self) -> list[HVACMode]:
207
  """Return a list of available HVACModes."""
208
  hvac_modes: list[HVACMode] = []
209
- if "regulation_modes" in self._gateway_data:
210
  hvac_modes.append(HVACMode.OFF)
211
 
212
- if self.device.get("available_schedules"):
213
  hvac_modes.append(HVACMode.AUTO)
214
 
215
  if self._api.cooling_present:
216
- if "regulation_modes" in self._gateway_data:
217
- if "heating" in self._gateway_data["regulation_modes"]:
218
  hvac_modes.append(HVACMode.HEAT)
219
- if "cooling" in self._gateway_data["regulation_modes"]:
220
  hvac_modes.append(HVACMode.COOL)
221
  else:
222
  hvac_modes.append(HVACMode.HEAT_COOL)
@@ -227,19 +262,19 @@
227
 
228
  @property
229
  @override
230
- def hvac_action(self) -> HVACAction:
231
  """Return the current running hvac operation if supported."""
232
  # Keep track of the previous hvac_action mode.
233
  # When no cooling available, _previous_action_mode is always heating
234
  if (
235
- "regulation_modes" in self._gateway_data
236
- and HVACAction.COOLING.value in self._gateway_data["regulation_modes"]
237
  ):
238
- mode = self._gateway_data["select_regulation_mode"]
239
  if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
240
  self._previous_action_mode = mode
241
 
242
- if (action := self.device.get("control_state")) is not None:
243
  return HVACAction(action)
244
 
245
  return HVACAction.IDLE
@@ -248,7 +283,7 @@
248
  @override
249
  def preset_mode(self) -> str | None:
250
  """Return the current preset mode."""
251
- return self.device.get("active_preset")
252
 
253
  @plugwise_command
254
  @override
@@ -256,11 +291,13 @@
256
  """Set new target temperature."""
257
  data: dict[str, Any] = {}
258
  if ATTR_TEMPERATURE in kwargs:
259
- data["setpoint"] = kwargs.get(ATTR_TEMPERATURE)
260
  if ATTR_TARGET_TEMP_HIGH in kwargs:
261
- data["setpoint_high"] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
262
  if ATTR_TARGET_TEMP_LOW in kwargs:
263
- data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
 
 
264
 
265
  if mode := kwargs.get(ATTR_HVAC_MODE):
266
  await self.async_set_hvac_mode(mode)
@@ -268,7 +305,7 @@
268
  await self._api.set_temperature(self._location, data)
269
 
270
  def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
271
- """Return the API regulation value for a manual HVAC mode, or None.
272
 
273
  The function inputs are limited to the HVACModes HEAT and COOL.
274
  """
@@ -282,6 +319,7 @@
282
  @override
283
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
284
  """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
 
285
  # Early exit if no mode change
286
  if hvac_mode == self.hvac_mode:
287
  return
 
12
  HVACAction,
13
  HVACMode,
14
  )
15
+ from homeassistant.const import (
16
+ ATTR_NAME,
17
+ ATTR_TEMPERATURE,
18
+ STATE_OFF,
19
+ STATE_ON,
20
+ UnitOfTemperature,
21
+ )
22
  from homeassistant.core import HomeAssistant, callback
23
  from homeassistant.exceptions import HomeAssistantError
24
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
25
  from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
26
 
27
+ from .const import (
28
+ ACTIVE_PRESET,
29
+ AVAILABLE_SCHEDULES,
30
+ CLIMATE_MODE,
31
+ CONTROL_STATE,
32
+ DEV_CLASS,
33
+ DOMAIN,
34
+ LOCATION,
35
+ LOGGER,
36
+ LOWER_BOUND,
37
+ MASTER_THERMOSTATS,
38
+ REGULATION_MODES,
39
+ RESOLUTION,
40
+ SELECT_REGULATION_MODE,
41
+ SENSORS,
42
+ TARGET_TEMP,
43
+ TARGET_TEMP_HIGH,
44
+ TARGET_TEMP_LOW,
45
+ THERMOSTAT,
46
+ UPPER_BOUND,
47
+ )
48
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
49
  from .entity import PlugwiseEntity
50
  from .util import plugwise_command
 
53
  PARALLEL_UPDATES = 0
54
 
55
 
56
+ async def async_setup_entry(
57
+ hass: HomeAssistant,
58
+ entry: PlugwiseConfigEntry,
59
+ async_add_entities: AddConfigEntryEntitiesCallback,
60
+ ) -> None:
61
+ """Set up Plugwise thermostats from a config entry."""
62
+ coordinator = entry.runtime_data
63
+
64
+ @callback
65
+ def _add_entities() -> None:
66
+ """Add Entities during init and runtime."""
67
+ if not coordinator.new_devices:
68
+ return
69
+
70
+ entities: list[PlugwiseClimateEntity] = []
71
+ gateway_name = coordinator.api.smile.name
72
+ for device_id in coordinator.new_devices:
73
+ device = coordinator.data[device_id]
74
+ if gateway_name == "Adam":
75
+ if device[DEV_CLASS] == "climate":
76
+ entities.append(
77
+ PlugwiseClimateEntity(coordinator, device_id)
78
+ )
79
+ LOGGER.debug("Add climate %s", device[ATTR_NAME])
80
+ elif device[DEV_CLASS] in MASTER_THERMOSTATS:
81
+ entities.append(
82
+ PlugwiseClimateEntity(coordinator, device_id)
83
+ )
84
+ LOGGER.debug("Add climate %s", device[ATTR_NAME])
85
+
86
+ async_add_entities(entities)
87
+
88
+ _add_entities()
89
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
90
+
91
+
92
  def _check_for_schedule(active: bool, last_active: str | None) -> None:
93
  """Raise a HAError when no thermostat schedule has been set."""
94
  if not active and last_active is None:
 
110
  """Return a dict representation of the text data."""
111
  return asdict(self)
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
115
  """Representation of a Plugwise thermostat."""
116
 
117
+ _attr_has_entity_name = True
118
  _attr_name = None
119
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
120
  _attr_translation_key = DOMAIN
121
+ _enable_turn_on_off_backwards_compatibility = False
122
 
123
  def __init__(
124
  self,
 
127
  ) -> None:
128
  """Set up the Plugwise API."""
129
  super().__init__(coordinator, device_id)
 
130
 
131
  self._api = coordinator.api
132
  gateway_id: str = self._api.gateway_id
133
  self._gateway_data = coordinator.data[gateway_id]
134
  self._last_active_schedule: str | None = None
135
  self._location = device_id
136
+ if (location := self.device.get(LOCATION)) is not None:
137
  self._location = location
138
  self._previous_action_mode = HVACAction.HEATING.value
139
 
140
+ self._attr_max_temp = min(self.device.get(THERMOSTAT, {}).get(UPPER_BOUND, 35.0), 35.0)
141
+ self._attr_min_temp = self.device.get(THERMOSTAT, {}).get(LOWER_BOUND, 0.0)
142
+ # Ensure we don't drop below 0.1
143
+ self._attr_target_temperature_step = max(
144
+ self.device.get(THERMOSTAT, {}).get(RESOLUTION, 0.5), 0.1
145
+ )
146
+ entity_name = f"{self.device[ATTR_NAME]}".lower()
147
+ self._attr_unique_id = f"{device_id}-{entity_name}"
148
+
149
  # Determine supported features
150
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
151
+ if (
152
+ self._api.cooling_present
153
+ and self._api.smile.name != "Adam"
154
+ ):
155
  self._attr_supported_features = (
156
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
157
  )
 
159
  self._attr_supported_features |= (
160
  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
161
  )
162
+ if presets := self.device.get("preset_modes", None): # can be NONE
163
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
164
+ self._attr_preset_modes = presets
 
 
 
 
 
 
 
165
 
166
  @override
167
  async def async_added_to_hass(self) -> None:
168
  """Run when entity about to be added."""
 
169
 
170
+ extra_data = await self.async_get_last_extra_data()
171
+ if extra_data is not None:
172
+ data = extra_data.as_dict()
173
+ self._last_active_schedule = data.get("last_active_schedule")
 
174
  self._previous_action_mode = (
175
+ data.get("previous_action_mode") or HVACAction.HEATING.value
176
  )
177
 
178
+ await super().async_added_to_hass()
179
+
180
  @property
181
  @override
182
  def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
183
  """Return text specific state data to be restored."""
184
  return PlugwiseClimateExtraStoredData(
185
+ self._last_active_schedule,
186
+ self._previous_action_mode,
187
  )
188
 
189
  @property
190
  @override
191
  def current_temperature(self) -> float | None:
192
  """Return the current temperature."""
193
+ return self.device.get(SENSORS, {}).get(ATTR_TEMPERATURE)
194
 
195
  @property
196
  @override
197
+ def target_temperature(self) -> float | None:
198
  """Return the temperature we try to reach.
199
 
200
  Connected to the HVACMode combination of AUTO-HEAT.
201
  """
202
 
203
+ return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP)
204
 
205
  @property
206
  @override
207
+ def target_temperature_high(self) -> float | None:
208
  """Return the temperature we try to reach in case of cooling.
209
 
210
  Connected to the HVACMode combination of AUTO-HEAT_COOL.
211
  """
212
+ return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP_HIGH)
213
 
214
  @property
215
  @override
216
+ def target_temperature_low(self) -> float | None:
217
  """Return the heating temperature we try to reach in case of heating.
218
 
219
  Connected to the HVACMode combination AUTO-HEAT_COOL.
220
  """
221
+ return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP_LOW)
222
 
223
  @property
224
  @override
225
  def hvac_mode(self) -> HVACMode:
226
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
227
+ mode = self.device.get(CLIMATE_MODE)
228
+ if mode is None:
229
+ return HVACMode.HEAT # pragma: no cover
230
+ try:
231
+ hvac = HVACMode(mode)
232
+ except ValueError: # pragma: no cover
233
+ return HVACMode.HEAT # pragma: no cover
234
+ if hvac not in self.hvac_modes:
235
+ return HVACMode.HEAT # pragma: no cover
236
+
237
+ return hvac
238
 
239
  @property
240
  @override
241
  def hvac_modes(self) -> list[HVACMode]:
242
  """Return a list of available HVACModes."""
243
  hvac_modes: list[HVACMode] = []
244
+ if REGULATION_MODES in self._gateway_data:
245
  hvac_modes.append(HVACMode.OFF)
246
 
247
+ if self.device.get(AVAILABLE_SCHEDULES, []):
248
  hvac_modes.append(HVACMode.AUTO)
249
 
250
  if self._api.cooling_present:
251
+ if REGULATION_MODES in self._gateway_data:
252
+ if "heating" in self._gateway_data[REGULATION_MODES]:
253
  hvac_modes.append(HVACMode.HEAT)
254
+ if "cooling" in self._gateway_data[REGULATION_MODES]:
255
  hvac_modes.append(HVACMode.COOL)
256
  else:
257
  hvac_modes.append(HVACMode.HEAT_COOL)
 
262
 
263
  @property
264
  @override
265
+ def hvac_action(self) -> HVACAction: # pw-beta add to Core
266
  """Return the current running hvac operation if supported."""
267
  # Keep track of the previous hvac_action mode.
268
  # When no cooling available, _previous_action_mode is always heating
269
  if (
270
+ REGULATION_MODES in self._gateway_data
271
+ and HVACAction.COOLING.value in self._gateway_data[REGULATION_MODES]
272
  ):
273
+ mode = self._gateway_data[SELECT_REGULATION_MODE]
274
  if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
275
  self._previous_action_mode = mode
276
 
277
+ if (action := self.device.get(CONTROL_STATE)) is not None:
278
  return HVACAction(action)
279
 
280
  return HVACAction.IDLE
 
283
  @override
284
  def preset_mode(self) -> str | None:
285
  """Return the current preset mode."""
286
+ return self.device.get(ACTIVE_PRESET)
287
 
288
  @plugwise_command
289
  @override
 
291
  """Set new target temperature."""
292
  data: dict[str, Any] = {}
293
  if ATTR_TEMPERATURE in kwargs:
294
+ data[TARGET_TEMP] = kwargs.get(ATTR_TEMPERATURE)
295
  if ATTR_TARGET_TEMP_HIGH in kwargs:
296
+ data[TARGET_TEMP_HIGH] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
297
  if ATTR_TARGET_TEMP_LOW in kwargs:
298
+ data[TARGET_TEMP_LOW] = kwargs.get(ATTR_TARGET_TEMP_LOW)
299
+
300
+ # Upstream removed input-valid check
301
 
302
  if mode := kwargs.get(ATTR_HVAC_MODE):
303
  await self.async_set_hvac_mode(mode)
 
305
  await self._api.set_temperature(self._location, data)
306
 
307
  def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
308
+ """Return the API regulation value for a manual HVAC mode.
309
 
310
  The function inputs are limited to the HVACModes HEAT and COOL.
311
  """
 
319
  @override
320
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
321
  """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
322
+
323
  # Early exit if no mode change
324
  if hvac_mode == self.hvac_mode:
325
  return
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/config_flow.py RENAMED
@@ -1,5 +1,8 @@
1
  """Config flow for Plugwise integration."""
2
 
 
 
 
3
  import logging
4
  from typing import Any, Self, override
5
 
@@ -14,7 +17,15 @@
14
  )
15
  import voluptuous as vol
16
 
17
- from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
 
 
 
 
 
 
 
 
18
  from homeassistant.const import (
19
  ATTR_CONFIGURATION_URL,
20
  CONF_BASE,
@@ -22,9 +33,13 @@
22
  CONF_NAME,
23
  CONF_PASSWORD,
24
  CONF_PORT,
 
25
  CONF_USERNAME,
26
  )
27
- from homeassistant.core import HomeAssistant
 
 
 
28
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
29
  from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
30
 
@@ -30,22 +45,35 @@
30
 
31
  from .const import (
32
  ANNA_WITH_ADAM,
 
33
  DEFAULT_PORT,
 
34
  DEFAULT_USERNAME,
35
  DOMAIN,
36
  FLOW_SMILE,
37
  FLOW_STRETCH,
 
 
38
  SMILE,
39
  SMILE_OPEN_THERM,
40
  SMILE_THERMO,
41
  STRETCH,
42
  STRETCH_USERNAME,
43
- UNKNOWN_SMILE,
 
 
44
  ZEROCONF_MAP,
45
  )
46
 
 
 
 
 
 
47
  _LOGGER = logging.getLogger(__name__)
48
 
 
 
49
  SMILE_RECONF_SCHEMA = vol.Schema(
50
  {
51
  vol.Required(CONF_HOST): str,
@@ -53,21 +81,31 @@
53
  )
54
 
55
 
56
- def smile_user_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
57
  """Generate base schema for gateways."""
58
- schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
59
-
60
- if not discovery_info:
61
- schema = schema.extend(
62
  {
63
  vol.Required(CONF_HOST): str,
 
64
  vol.Required(CONF_USERNAME, default=SMILE): vol.In(
65
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
66
  ),
67
  }
68
  )
69
 
70
- return schema
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
@@ -101,7 +139,7 @@
101
  errors[CONF_BASE] = "invalid_auth"
102
  except InvalidSetupError:
103
  errors[CONF_BASE] = "invalid_setup"
104
- except InvalidXMLError, ResponseError:
105
  errors[CONF_BASE] = "response_error"
106
  except UnsupportedDeviceError:
107
  errors[CONF_BASE] = "unsupported"
@@ -117,9 +155,10 @@
117
  """Handle a config flow for Plugwise Smile."""
118
 
119
  VERSION = 1
 
120
 
121
  discovery_info: ZeroconfServiceInfo | None = None
122
- product: str = UNKNOWN_SMILE
123
  _username: str = DEFAULT_USERNAME
124
 
125
  @override
@@ -129,17 +168,21 @@
129
  """Prepare configuration for a discovered Plugwise Smile."""
130
  self.discovery_info = discovery_info
131
  _properties = discovery_info.properties
132
-
 
133
  unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
 
 
 
134
  if config_entry := await self.async_set_unique_id(unique_id):
135
  try:
136
  await validate_input(
137
  self.hass,
138
  {
139
  CONF_HOST: discovery_info.host,
 
140
  CONF_PORT: discovery_info.port,
141
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
142
- CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
143
  },
144
  )
145
  except Exception: # noqa: BLE001
@@ -152,12 +195,6 @@
152
  }
153
  )
154
 
155
- if DEFAULT_USERNAME not in unique_id:
156
- self._username = STRETCH_USERNAME
157
- self.product = _product = _properties.get("product", UNKNOWN_SMILE)
158
- _version = _properties.get("version", "n/a")
159
- _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
160
-
161
  # This is an Anna, but we already have config entries.
162
  # Assuming that the user has already configured Adam, aborting discovery.
163
  if self._async_current_entries() and _product == SMILE_THERMO:
@@ -167,18 +204,20 @@
167
  # In that case, we need to cancel the Anna flow, as the Adam should
168
  # be added.
169
  if self.hass.config_entries.flow.async_has_matching_flow(self):
170
- return self.async_abort(reason=ANNA_WITH_ADAM)
171
 
 
172
  self.context.update(
173
  {
174
- "title_placeholders": {CONF_NAME: _name},
175
  ATTR_CONFIGURATION_URL: (
176
  f"http://{discovery_info.host}:{discovery_info.port}"
177
- ),
178
  }
179
  )
180
  return await self.async_step_user()
181
 
 
182
  @override
183
  def is_matching(self, other_flow: Self) -> bool:
184
  """Return True if other_flow is matching this flow."""
@@ -192,6 +231,7 @@
192
 
193
  return False
194
 
 
195
  @override
196
  async def async_step_user(
197
  self, user_input: dict[str, Any] | None = None
@@ -209,18 +249,19 @@
209
  api, errors = await verify_connection(self.hass, user_input)
210
  if api:
211
  await self.async_set_unique_id(
212
- api.smile.hostname or api.gateway_id,
213
- raise_on_progress=False,
214
  )
215
  self._abort_if_unique_id_configured()
216
  return self.async_create_entry(title=api.smile.name, data=user_input)
217
 
 
218
  return self.async_show_form(
219
  step_id=SOURCE_USER,
220
- data_schema=smile_user_schema(self.discovery_info),
221
  errors=errors,
222
  )
223
 
 
224
  async def async_step_reconfigure(
225
  self, user_input: dict[str, Any] | None = None
226
  ) -> ConfigFlowResult:
@@ -230,7 +271,7 @@
230
  reconfigure_entry = self._get_reconfigure_entry()
231
 
232
  if user_input:
233
- # Keep current username and password
234
  full_input = {
235
  CONF_HOST: user_input.get(CONF_HOST),
236
  CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
@@ -241,8 +282,7 @@
241
  api, errors = await verify_connection(self.hass, full_input)
242
  if api:
243
  await self.async_set_unique_id(
244
- api.smile.hostname or api.gateway_id,
245
- raise_on_progress=False,
246
  )
247
  self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
248
  return self.async_update_reload_and_abort(
@@ -259,3 +299,67 @@
259
  description_placeholders={"title": reconfigure_entry.title},
260
  errors=errors,
261
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Config flow for Plugwise integration."""
2
 
3
+ # pylint: disable=home-assistant-config-flow-polling-field
4
+
5
+ from copy import deepcopy
6
  import logging
7
  from typing import Any, Self, override
8
 
 
17
  )
18
  import voluptuous as vol
19
 
20
+ from homeassistant.config_entries import (
21
+ SOURCE_USER,
22
+ ConfigEntry,
23
+ ConfigFlow,
24
+ ConfigFlowResult,
25
+ OptionsFlow,
26
+ )
27
+
28
+ # Upstream
29
  from homeassistant.const import (
30
  ATTR_CONFIGURATION_URL,
31
  CONF_BASE,
 
33
  CONF_NAME,
34
  CONF_PASSWORD,
35
  CONF_PORT,
36
+ CONF_SCAN_INTERVAL,
37
  CONF_USERNAME,
38
  )
39
+
40
+ # Upstream
41
+ from homeassistant.core import HomeAssistant, callback
42
+ from homeassistant.helpers import config_validation as cv
43
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
44
  from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
45
 
 
45
 
46
  from .const import (
47
  ANNA_WITH_ADAM,
48
+ CONF_REFRESH_INTERVAL, # pw-beta option
49
  DEFAULT_PORT,
50
+ DEFAULT_UPDATE_INTERVAL,
51
  DEFAULT_USERNAME,
52
  DOMAIN,
53
  FLOW_SMILE,
54
  FLOW_STRETCH,
55
+ INIT,
56
+ P1_UPDATE_INTERVAL,
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
  vol.Required(CONF_USERNAME, default=SMILE): vol.In(
92
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
93
  ),
94
  }
95
  )
96
 
97
+ if isinstance(cf_input, ZeroconfServiceInfo):
98
+ return vol.Schema({vol.Required(CONF_PASSWORD): str})
99
+
100
+ return vol.Schema(
101
+ {
102
+ vol.Required(CONF_HOST, default=cf_input[CONF_HOST]): str,
103
+ vol.Required(CONF_PASSWORD, default=cf_input[CONF_PASSWORD]): str,
104
+ vol.Required(CONF_USERNAME, default=cf_input[CONF_USERNAME]): vol.In(
105
+ {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
106
+ ),
107
+ }
108
+ )
109
 
110
 
111
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
 
139
  errors[CONF_BASE] = "invalid_auth"
140
  except InvalidSetupError:
141
  errors[CONF_BASE] = "invalid_setup"
142
+ except (InvalidXMLError, ResponseError):
143
  errors[CONF_BASE] = "response_error"
144
  except UnsupportedDeviceError:
145
  errors[CONF_BASE] = "unsupported"
 
155
  """Handle a config flow for Plugwise Smile."""
156
 
157
  VERSION = 1
158
+ MINOR_VERSION = 1
159
 
160
  discovery_info: ZeroconfServiceInfo | None = None
161
+ product: str = "Unknown Smile"
162
  _username: str = DEFAULT_USERNAME
163
 
164
  @override
 
168
  """Prepare configuration for a discovered Plugwise Smile."""
169
  self.discovery_info = discovery_info
170
  _properties = discovery_info.properties
171
+ _version = _properties.get(VERSION, "n/a")
172
+ self.product = _product = _properties.get("product", "Unknown Smile")
173
  unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
174
+ if DEFAULT_USERNAME not in unique_id:
175
+ self._username = STRETCH_USERNAME
176
+
177
  if config_entry := await self.async_set_unique_id(unique_id):
178
  try:
179
  await validate_input(
180
  self.hass,
181
  {
182
  CONF_HOST: discovery_info.host,
183
+ CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
184
  CONF_PORT: discovery_info.port,
185
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
 
186
  },
187
  )
188
  except Exception: # noqa: BLE001
 
195
  }
196
  )
197
 
 
 
 
 
 
 
198
  # This is an Anna, but we already have config entries.
199
  # Assuming that the user has already configured Adam, aborting discovery.
200
  if self._async_current_entries() and _product == SMILE_THERMO:
 
204
  # In that case, we need to cancel the Anna flow, as the Adam should
205
  # be added.
206
  if self.hass.config_entries.flow.async_has_matching_flow(self):
207
+ return self.async_abort(reason="anna_with_adam")
208
 
209
+ _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
210
  self.context.update(
211
  {
212
+ TITLE_PLACEHOLDERS: {CONF_NAME: _name},
213
  ATTR_CONFIGURATION_URL: (
214
  f"http://{discovery_info.host}:{discovery_info.port}"
215
+ )
216
  }
217
  )
218
  return await self.async_step_user()
219
 
220
+
221
  @override
222
  def is_matching(self, other_flow: Self) -> bool:
223
  """Return True if other_flow is matching this flow."""
 
231
 
232
  return False
233
 
234
+
235
  @override
236
  async def async_step_user(
237
  self, user_input: dict[str, Any] | None = None
 
249
  api, errors = await verify_connection(self.hass, user_input)
250
  if api:
251
  await self.async_set_unique_id(
252
+ api.smile.hostname or api.gateway_id, raise_on_progress=False
 
253
  )
254
  self._abort_if_unique_id_configured()
255
  return self.async_create_entry(title=api.smile.name, data=user_input)
256
 
257
+ configure_input = self.discovery_info or user_input
258
  return self.async_show_form(
259
  step_id=SOURCE_USER,
260
+ data_schema=smile_user_schema(configure_input),
261
  errors=errors,
262
  )
263
 
264
+
265
  async def async_step_reconfigure(
266
  self, user_input: dict[str, Any] | None = None
267
  ) -> ConfigFlowResult:
 
271
  reconfigure_entry = self._get_reconfigure_entry()
272
 
273
  if user_input:
274
+ # Redefine ingest existing username and password
275
  full_input = {
276
  CONF_HOST: user_input.get(CONF_HOST),
277
  CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
 
282
  api, errors = await verify_connection(self.hass, full_input)
283
  if api:
284
  await self.async_set_unique_id(
285
+ api.smile.hostname or api.gateway_id, raise_on_progress=False
 
286
  )
287
  self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
288
  return self.async_update_reload_and_abort(
 
299
  description_placeholders={"title": reconfigure_entry.title},
300
  errors=errors,
301
  )
302
+
303
+
304
+ @callback
305
+ @override
306
+ @staticmethod
307
+ def async_get_options_flow(
308
+ config_entry: PlugwiseConfigEntry,
309
+ ) -> PlugwiseOptionsFlowHandler: # pw-beta options
310
+ """Get the options flow for this handler."""
311
+ return PlugwiseOptionsFlowHandler(config_entry)
312
+
313
+
314
+ # pw-beta - change the scan-interval via CONFIGURE
315
+ # pw-beta - change the frontend refresh interval via CONFIGURE
316
+ class PlugwiseOptionsFlowHandler(OptionsFlow): # pw-beta options
317
+ """Plugwise option flow."""
318
+
319
+ def __init__(self, config_entry: ConfigEntry) -> None:
320
+ """Initialize options flow."""
321
+ self.options = deepcopy(dict(config_entry.options))
322
+
323
+ def _create_options_schema(self, coordinator: PlugwiseDataUpdateCoordinator) -> vol.Schema:
324
+ interval = DEFAULT_UPDATE_INTERVAL
325
+ if coordinator.api.smile.type == "power":
326
+ interval = P1_UPDATE_INTERVAL
327
+ schema = {
328
+ vol.Optional(
329
+ CONF_SCAN_INTERVAL,
330
+ default=self.options.get(CONF_SCAN_INTERVAL, interval.seconds),
331
+ ): vol.All(cv.positive_int, vol.Clamp(min=10)),
332
+ } # pw-beta
333
+
334
+ if coordinator.api.smile.type == THERMOSTAT:
335
+ schema.update({
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
@@ -6,28 +6,158 @@
6
 
7
  from homeassistant.const import Platform
8
 
 
 
9
  DOMAIN: Final = "plugwise"
10
 
11
  LOGGER = logging.getLogger(__package__)
12
 
13
- ANNA_WITH_ADAM: Final = "anna_with_adam"
14
  API: Final = "api"
15
- AVAILABLE: Final = "available"
16
- DEV_CLASS: Final = "dev_class"
17
- FLOW_SMILE: Final = "smile (Adam/Anna/P1)"
18
- FLOW_STRETCH: Final = "stretch (Stretch)"
19
- FLOW_TYPE: Final = "flow_type"
20
  GATEWAY: Final = "gateway"
21
  LOCATION: Final = "location"
22
- PW_TYPE: Final = "plugwise_type"
23
  REBOOT: Final = "reboot"
24
  SMILE: Final = "smile"
25
- SMILE_OPEN_THERM: Final = "smile_open_therm"
26
- SMILE_THERMO: Final = "smile_thermo"
27
  STRETCH: Final = "stretch"
28
  STRETCH_USERNAME: Final = "stretch"
29
- UNKNOWN_SMILE: Final = "Unknown Smile"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
 
31
  PLATFORMS: Final[list[str]] = [
32
  Platform.BINARY_SENSOR,
33
  Platform.BUTTON,
@@ -36,7 +166,20 @@
36
  Platform.SELECT,
37
  Platform.SENSOR,
38
  Platform.SWITCH,
 
39
  ]
 
 
 
 
 
 
 
 
 
 
 
 
40
  ZEROCONF_MAP: Final[dict[str, str]] = {
41
  "smile": "Smile P1",
42
  "smile_thermo": "Smile Anna",
@@ -55,7 +198,7 @@
55
  "select_gateway_mode",
56
  "select_regulation_mode",
57
  "select_schedule",
58
- "select_zone_profile",
59
  ]
60
  type SelectOptionsType = Literal[
61
  "available_schedules",
@@ -64,25 +207,3 @@
64
  "regulation_modes",
65
  "zone_profiles",
66
  ]
67
-
68
- # Default directives
69
- DEFAULT_MAX_TEMP: Final = 30
70
- DEFAULT_MIN_TEMP: Final = 4
71
- DEFAULT_PORT: Final = 80
72
- DEFAULT_UPDATE_INTERVAL = timedelta(seconds=60)
73
- DEFAULT_USERNAME: Final = "smile"
74
- P1_UPDATE_INTERVAL = timedelta(seconds=10)
75
-
76
- MASTER_THERMOSTATS: Final[list[str]] = [
77
- "thermostat",
78
- "thermostatic_radiator_valve",
79
- "zone_thermometer",
80
- "zone_thermostat",
81
- ]
82
-
83
- # Select constants
84
- SELECT_DHW_MODE: Final = "select_dhw_mode"
85
- SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
86
- SELECT_REGULATION_MODE: Final = "select_regulation_mode"
87
- SELECT_SCHEDULE: Final = "select_schedule"
88
- SELECT_ZONE_PROFILE: Final = "select_zone_profile"
 
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
+ CONFIG_ENTRY: Final = "config_entry" # pw-beta service
18
+ CONF_HOMEKIT_EMULATION: Final = "homekit_emulation" # pw-beta options
19
+ CONF_REFRESH_INTERVAL: Final = "refresh_interval" # pw-beta options
20
+ CONF_MANUAL_PATH: Final = "Enter Manually"
21
  GATEWAY: Final = "gateway"
22
  LOCATION: Final = "location"
23
+ MAC_ADDRESS: Final = "mac_address"
24
  REBOOT: Final = "reboot"
25
  SMILE: Final = "smile"
 
 
26
  STRETCH: Final = "stretch"
27
  STRETCH_USERNAME: Final = "stretch"
28
+ SWITCH_GROUPS: Final[tuple[str, str]] = ("report", "switching")
29
+ UNIQUE_IDS: Final = "unique_ids"
30
+ ZIGBEE_MAC_ADDRESS: Final = "zigbee_mac_address"
31
+
32
+ # Binary Sensor constants
33
+ BINARY_SENSORS: Final = "binary_sensors"
34
+ BATTERY_STATE: Final = "low_battery"
35
+ COMPRESSOR_STATE: Final = "compressor_state"
36
+ COOLING_ENABLED: Final = "cooling_enabled"
37
+ COOLING_STATE: Final = "cooling_state"
38
+ DHW_STATE: Final = "dhw_state"
39
+ FLAME_STATE: Final = "flame_state"
40
+ HEATING_STATE: Final = "heating_state"
41
+ PLUGWISE_NOTIFICATION: Final = "plugwise_notification"
42
+ SECONDARY_BOILER_STATE: Final = "secondary_boiler_state"
43
+
44
+ # Climate constants
45
+ ACTIVE_PRESET: Final = "active_preset"
46
+ CLIMATE_MODE: Final = "climate_mode"
47
+ CONTROL_STATE: Final = "control_state"
48
+ COOLING_PRESENT: Final ="cooling_present"
49
+ DEV_CLASS: Final = "dev_class"
50
+ NONE : Final = "None"
51
+ TARGET_TEMP: Final = "setpoint"
52
+ TARGET_TEMP_HIGH: Final = "setpoint_high"
53
+ TARGET_TEMP_LOW: Final = "setpoint_low"
54
+ THERMOSTAT: Final = "thermostat"
55
+
56
+ # Config_flow constants
57
+ ANNA_WITH_ADAM: Final = "anna_with_adam"
58
+ CONTEXT: Final = "context"
59
+ FLOW_ID: Final = "flow_id"
60
+ FLOW_NET: Final = "Network: Smile/Stretch"
61
+ FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
62
+ FLOW_STRETCH: Final = "Stretch (Stretch)"
63
+ FLOW_TYPE: Final = "flow_type"
64
+ INIT: Final = "init"
65
+ PRODUCT: Final = "product"
66
+ SMILE_OPEN_THERM: Final = "smile_open_therm"
67
+ SMILE_THERMO: Final = "smile_thermo"
68
+ TITLE_PLACEHOLDERS: Final = "title_placeholders"
69
+ VERSION: Final = "version"
70
+
71
+ # Entity constants
72
+ AVAILABLE: Final = "available"
73
+ FIRMWARE: Final = "firmware"
74
+ HARDWARE: Final = "hardware"
75
+ MODEL: Final = "model"
76
+ MODEL_ID: Final = "model_id"
77
+ VENDOR: Final = "vendor"
78
+
79
+ # Number constants
80
+ MAX_BOILER_TEMP: Final = "maximum_boiler_temperature"
81
+ MAX_DHW_TEMP: Final = "max_dhw_temperature"
82
+ LOWER_BOUND: Final = "lower_bound"
83
+ RESOLUTION: Final = "resolution"
84
+ TEMPERATURE_OFFSET: Final = "temperature_offset"
85
+ UPPER_BOUND: Final = "upper_bound"
86
+
87
+ # Sensor constants
88
+ DHW_TEMP: Final = "dhw_temperature"
89
+ DHW_SETPOINT: Final = "domestic_hot_water_setpoint"
90
+ EL_CONSUMED: Final = "electricity_consumed"
91
+ EL_CONS_INTERVAL: Final = "electricity_consumed_interval"
92
+ EL_CONS_OP_CUMULATIVE: Final = "electricity_consumed_off_peak_cumulative"
93
+ EL_CONS_OP_INTERVAL: Final = "electricity_consumed_off_peak_interval"
94
+ EL_CONS_OP_POINT: Final = "electricity_consumed_off_peak_point"
95
+ EL_CONS_P_CUMULATIVE: Final = "electricity_consumed_peak_cumulative"
96
+ EL_CONS_P_INTERVAL: Final = "electricity_consumed_peak_interval"
97
+ EL_CONS_P_POINT: Final = "electricity_consumed_peak_point"
98
+ EL_CONS_POINT: Final = "electricity_consumed_point"
99
+ EL_PH1_CONSUMED: Final = "electricity_phase_one_consumed"
100
+ EL_PH2_CONSUMED: Final = "electricity_phase_two_consumed"
101
+ EL_PH3_CONSUMED: Final = "electricity_phase_three_consumed"
102
+ EL_PH1_PRODUCED: Final = "electricity_phase_one_produced"
103
+ EL_PH2_PRODUCED: Final = "electricity_phase_two_produced"
104
+ EL_PH3_PRODUCED: Final = "electricity_phase_three_produced"
105
+ EL_PRODUCED: Final = "electricity_produced"
106
+ EL_PROD_INTERVAL: Final = "electricity_produced_interval"
107
+ EL_PROD_OP_CUMULATIVE: Final = "electricity_produced_off_peak_cumulative"
108
+ EL_PROD_OP_INTERVAL: Final = "electricity_produced_off_peak_interval"
109
+ EL_PROD_OP_POINT: Final = "electricity_produced_off_peak_point"
110
+ EL_PROD_P_CUMULATIVE: Final = "electricity_produced_peak_cumulative"
111
+ EL_PROD_P_INTERVAL: Final = "electricity_produced_peak_interval"
112
+ EL_PROD_P_POINT: Final = "electricity_produced_peak_point"
113
+ EL_PROD_POINT: Final = "electricity_produced_point"
114
+ GAS_CONS_CUMULATIVE: Final = "gas_consumed_cumulative"
115
+ GAS_CONS_INTERVAL: Final = "gas_consumed_interval"
116
+ INTENDED_BOILER_TEMP: Final = "intended_boiler_temperature"
117
+ MOD_LEVEL: Final = "modulation_level"
118
+ NET_EL_POINT: Final = "net_electricity_point"
119
+ NET_EL_CUMULATIVE: Final = "net_electricity_cumulative"
120
+ OUTDOOR_AIR_TEMP: Final = "outdoor_air_temperature"
121
+ OUTDOOR_TEMP: Final = "outdoor_temperature"
122
+ RETURN_TEMP: Final = "return_temperature"
123
+ SENSORS: Final = "sensors"
124
+ TEMP_DIFF: Final = "temperature_difference"
125
+ VALVE_POS: Final = "valve_position"
126
+ VOLTAGE_PH1: Final = "voltage_phase_one"
127
+ VOLTAGE_PH2: Final = "voltage_phase_two"
128
+ VOLTAGE_PH3: Final = "voltage_phase_three"
129
+ WATER_TEMP: Final = "water_temperature"
130
+ WATER_PRESSURE: Final = "water_pressure"
131
+
132
+ # Select constants
133
+ AVAILABLE_SCHEDULES: Final = "available_schedules"
134
+ DHW_MODE: Final = "dhw_mode"
135
+ DHW_MODES: Final = "dhw_modes"
136
+ GATEWAY_MODES: Final = "gateway_modes"
137
+ REGULATION_MODES: Final = "regulation_modes"
138
+ ZONE_PROFILES: Final = "zone_profiles"
139
+ SELECT_DHW_MODE: Final = "select_dhw_mode"
140
+ SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
141
+ SELECT_REGULATION_MODE: Final = "select_regulation_mode"
142
+ SELECT_SCHEDULE: Final = "select_schedule"
143
+ SELECT_ZONE_PROFILE: Final = "select_zone_profile"
144
+
145
+ # Switch constants
146
+ DHW_CM_SWITCH: Final = "dhw_cm_switch"
147
+ LOCK: Final = "lock"
148
+ MEMBERS: Final ="members"
149
+ RELAY: Final = "relay"
150
+ COOLING_ENA_SWITCH: Final ="cooling_ena_switch"
151
+ SWITCHES: Final = "switches"
152
+
153
+ # Default directives
154
+ DEFAULT_PORT: Final[int] = 80
155
+ DEFAULT_TIMEOUT: Final[int] = 30
156
+ DEFAULT_UPDATE_INTERVAL: Final = timedelta(seconds=60)
157
+ DEFAULT_USERNAME: Final = "smile"
158
+ P1_UPDATE_INTERVAL: Final = timedelta(seconds=10)
159
 
160
+ # --- Const for Plugwise Smile and Stretch
161
  PLATFORMS: Final[list[str]] = [
162
  Platform.BINARY_SENSOR,
163
  Platform.BUTTON,
 
166
  Platform.SELECT,
167
  Platform.SENSOR,
168
  Platform.SWITCH,
169
+ Platform.WATER_HEATER,
170
  ]
171
+ SERVICE_DELETE: Final = "delete_notification"
172
+ SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
173
+
174
+ # Climate const:
175
+ MASTER_THERMOSTATS: Final[list[str]] = [
176
+ "thermostat",
177
+ "zone_thermometer",
178
+ "zone_thermostat",
179
+ "thermostatic_radiator_valve",
180
+ ]
181
+
182
+ # Config_flow const:
183
  ZEROCONF_MAP: Final[dict[str, str]] = {
184
  "smile": "Smile P1",
185
  "smile_thermo": "Smile Anna",
 
198
  "select_gateway_mode",
199
  "select_regulation_mode",
200
  "select_schedule",
201
+ "select_zone_profile"
202
  ]
203
  type SelectOptionsType = Literal[
204
  "available_schedules",
 
207
  "regulation_modes",
208
  "zone_profiles",
209
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/coordinator.py RENAMED
@@ -1,8 +1,8 @@
1
  """DataUpdateCoordinator for Plugwise."""
2
 
 
3
  from typing import override
4
 
5
- from packaging.version import Version
6
  from plugwise import GwEntityData, Smile
7
  from plugwise.exceptions import (
8
  ConnectionFailedError,
@@ -15,21 +15,29 @@
15
  )
16
 
17
  from homeassistant.config_entries import ConfigEntry
18
- from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
 
 
 
 
 
 
19
  from homeassistant.core import HomeAssistant
20
  from homeassistant.exceptions import ConfigEntryError
21
  from homeassistant.helpers import device_registry as dr
22
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
23
  from homeassistant.helpers.debounce import Debouncer
24
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
25
 
26
  from .const import (
27
- DEFAULT_PORT,
28
  DEFAULT_UPDATE_INTERVAL,
29
- DEFAULT_USERNAME,
30
  DOMAIN,
 
31
  LOGGER,
32
  P1_UPDATE_INTERVAL,
 
33
  )
34
 
35
  type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
@@ -40,29 +48,36 @@
40
 
41
  config_entry: PlugwiseConfigEntry
42
 
43
- def __init__(self, hass: HomeAssistant, config_entry: PlugwiseConfigEntry) -> None:
 
 
 
 
 
44
  """Initialize the coordinator."""
45
  super().__init__(
46
  hass,
47
  LOGGER,
48
  config_entry=config_entry,
49
  name=DOMAIN,
 
 
50
  update_interval=DEFAULT_UPDATE_INTERVAL,
51
  # Don't refresh immediately, give the device time to process
52
  # the change in state before we query it.
53
  request_refresh_debouncer=Debouncer(
54
  hass,
55
  LOGGER,
56
- cooldown=1.5,
57
  immediate=False,
58
  ),
59
  )
60
 
61
  self.api = Smile(
62
  host=self.config_entry.data[CONF_HOST],
63
- username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
64
  password=self.config_entry.data[CONF_PASSWORD],
65
- port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
 
66
  websession=async_get_clientsession(hass, verify_ssl=False),
67
  )
68
  self._connected: bool = False
@@ -78,8 +93,15 @@
78
  """
79
  version = await self.api.connect()
80
  self._connected = isinstance(version, Version)
81
- if self._connected and self.api.smile.type == "power":
82
- self.update_interval = P1_UPDATE_INTERVAL
 
 
 
 
 
 
 
83
 
84
  @override
85
  async def _async_setup(self) -> None:
@@ -133,25 +155,27 @@
133
  translation_key="unsupported_firmware",
134
  ) from err
135
 
 
136
  self._add_remove_devices(data)
137
  self._update_device_firmware(data)
138
  return data
139
 
140
  def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
141
  """Add new Plugwise devices, remove non-existing devices."""
 
 
 
 
 
 
 
 
142
  set_of_data = set(data)
143
- # Check for new or removed devices,
144
- # 'new_devices' contains all devices present in 'data'
145
- # at init ('self._current_devices' is empty) this is
146
- # required for the proper initialization of all the
147
- # present platform entities.
148
  self.new_devices = set_of_data - self._current_devices
149
  for device_id in self.new_devices:
150
- self._firmware_list.setdefault(device_id, data[device_id].get("firmware"))
151
 
152
- current_devices = (
153
- self._stored_devices if not self._current_devices else self._current_devices
154
- )
155
  self._current_devices = set_of_data
156
  if removed_devices := (current_devices - set_of_data): # device(s) to remove
157
  self._remove_devices(removed_devices)
@@ -160,11 +184,7 @@
160
  """Clean registries when removed devices found."""
161
  device_reg = dr.async_get(self.hass)
162
  for device_id in removed_devices:
163
- if (
164
- device_entry := device_reg.async_get_device_by_identifier(
165
- (DOMAIN, device_id), self.config_entry.entry_id
166
- )
167
- ) is not None:
168
  device_reg.async_update_device(
169
  device_entry.id, remove_config_entry_id=self.config_entry.entry_id
170
  )
@@ -177,14 +197,15 @@
177
 
178
  self._firmware_list.pop(device_id, None)
179
 
 
180
  def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
181
  """Detect firmware changes and update the device registry."""
182
  for device_id, device in data.items():
183
  # Only update firmware when the key is present and not None, to avoid
184
  # wiping stored firmware on partial or transient updates.
185
- if "firmware" not in device:
186
  continue
187
- new_firmware = device.get("firmware")
188
  if new_firmware is None:
189
  continue
190
  if (
@@ -198,11 +219,7 @@
198
  def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> bool:
199
  """Update device sw_version in device_registry."""
200
  device_reg = dr.async_get(self.hass)
201
- if (
202
- device_entry := device_reg.async_get_device_by_identifier(
203
- (DOMAIN, device_id), self.config_entry.entry_id
204
- )
205
- ) is not None:
206
  device_reg.async_update_device(device_entry.id, sw_version=firmware)
207
  LOGGER.debug(
208
  "Firmware in device_registry updated for %s %s %s",
 
1
  """DataUpdateCoordinator for Plugwise."""
2
 
3
+ from datetime import timedelta
4
  from typing import override
5
 
 
6
  from plugwise import GwEntityData, Smile
7
  from plugwise.exceptions import (
8
  ConnectionFailedError,
 
15
  )
16
 
17
  from homeassistant.config_entries import ConfigEntry
18
+ from homeassistant.const import (
19
+ CONF_HOST,
20
+ CONF_PASSWORD,
21
+ CONF_PORT,
22
+ CONF_SCAN_INTERVAL, # pw-beta options
23
+ CONF_USERNAME,
24
+ )
25
  from homeassistant.core import HomeAssistant
26
  from homeassistant.exceptions import ConfigEntryError
27
  from homeassistant.helpers import device_registry as dr
28
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
29
  from homeassistant.helpers.debounce import Debouncer
30
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
31
+ from packaging.version import Version
32
 
33
  from .const import (
 
34
  DEFAULT_UPDATE_INTERVAL,
35
+ DEV_CLASS,
36
  DOMAIN,
37
+ FIRMWARE,
38
  LOGGER,
39
  P1_UPDATE_INTERVAL,
40
+ SWITCH_GROUPS,
41
  )
42
 
43
  type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
 
48
 
49
  config_entry: PlugwiseConfigEntry
50
 
51
+ def __init__(
52
+ self,
53
+ hass: HomeAssistant,
54
+ cooldown: float,
55
+ config_entry: PlugwiseConfigEntry,
56
+ ) -> None: # pw-beta cooldown
57
  """Initialize the coordinator."""
58
  super().__init__(
59
  hass,
60
  LOGGER,
61
  config_entry=config_entry,
62
  name=DOMAIN,
63
+ # Core directly updates from const's DEFAULT_SCAN_INTERVAL
64
+ # Upstream check correct progress for adjusting
65
  update_interval=DEFAULT_UPDATE_INTERVAL,
66
  # Don't refresh immediately, give the device time to process
67
  # the change in state before we query it.
68
  request_refresh_debouncer=Debouncer(
69
  hass,
70
  LOGGER,
71
+ cooldown=cooldown,
72
  immediate=False,
73
  ),
74
  )
75
 
76
  self.api = Smile(
77
  host=self.config_entry.data[CONF_HOST],
 
78
  password=self.config_entry.data[CONF_PASSWORD],
79
+ port=self.config_entry.data[CONF_PORT],
80
+ username=self.config_entry.data[CONF_USERNAME],
81
  websession=async_get_clientsession(hass, verify_ssl=False),
82
  )
83
  self._connected: bool = False
 
93
  """
94
  version = await self.api.connect()
95
  self._connected = isinstance(version, Version)
96
+ if self._connected:
97
+ if self.api.smile.type == "power":
98
+ self.update_interval = P1_UPDATE_INTERVAL
99
+ if (custom_time := self.config_entry.options.get(CONF_SCAN_INTERVAL)) is not None:
100
+ self.update_interval = timedelta(
101
+ seconds=int(custom_time)
102
+ ) # pragma: no cover # pw-beta options
103
+
104
+ LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
105
 
106
  @override
107
  async def _async_setup(self) -> None:
 
155
  translation_key="unsupported_firmware",
156
  ) from err
157
 
158
+ LOGGER.debug("%s data: %s", self.api.smile.name, data)
159
  self._add_remove_devices(data)
160
  self._update_device_firmware(data)
161
  return data
162
 
163
  def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
164
  """Add new Plugwise devices, remove non-existing devices."""
165
+ # Block switch-groups, use HA group helper instead to create switch-groups
166
+ for device_id, device in data.copy().items():
167
+ if device.get(DEV_CLASS) in SWITCH_GROUPS:
168
+ data.pop(device_id)
169
+
170
+ # Collect new or removed devices,
171
+ # 'new_devices' contains all devices present in 'data' at init ('self._current_devices' is empty)
172
+ # this is required for the initialization of the available platform entities.
173
  set_of_data = set(data)
 
 
 
 
 
174
  self.new_devices = set_of_data - self._current_devices
175
  for device_id in self.new_devices:
176
+ self._firmware_list.setdefault(device_id, data[device_id].get(FIRMWARE))
177
 
178
+ current_devices = self._stored_devices if not self._current_devices else self._current_devices
 
 
179
  self._current_devices = set_of_data
180
  if removed_devices := (current_devices - set_of_data): # device(s) to remove
181
  self._remove_devices(removed_devices)
 
184
  """Clean registries when removed devices found."""
185
  device_reg = dr.async_get(self.hass)
186
  for device_id in removed_devices:
187
+ if (device_entry := device_reg.async_get_device({(DOMAIN, device_id)})) is not None:
 
 
 
 
188
  device_reg.async_update_device(
189
  device_entry.id, remove_config_entry_id=self.config_entry.entry_id
190
  )
 
197
 
198
  self._firmware_list.pop(device_id, None)
199
 
200
+
201
  def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
202
  """Detect firmware changes and update the device registry."""
203
  for device_id, device in data.items():
204
  # Only update firmware when the key is present and not None, to avoid
205
  # wiping stored firmware on partial or transient updates.
206
+ if FIRMWARE not in device:
207
  continue
208
+ new_firmware = device.get(FIRMWARE)
209
  if new_firmware is None:
210
  continue
211
  if (
 
219
  def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> bool:
220
  """Update device sw_version in device_registry."""
221
  device_reg = dr.async_get(self.hass)
222
+ if (device_entry := device_reg.async_get_device({(DOMAIN, device_id)})) is not None:
 
 
 
 
223
  device_reg.async_update_device(device_entry.id, sw_version=firmware)
224
  LOGGER.debug(
225
  "Firmware in device_registry updated for %s %s %s",
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/entity.py RENAMED
@@ -2,7 +2,7 @@
2
 
3
  from typing import override
4
 
5
- from plugwise import GwEntityData
6
 
7
  from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
8
  from homeassistant.helpers.device_registry import (
@@ -12,7 +12,19 @@
12
  )
13
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
14
 
15
- from .const import AVAILABLE, DOMAIN
 
 
 
 
 
 
 
 
 
 
 
 
16
  from .coordinator import PlugwiseDataUpdateCoordinator
17
 
18
 
@@ -43,9 +55,9 @@
43
 
44
  # Build connections set
45
  connections = set()
46
- if mac := self.device.get("mac_address"):
47
  connections.add((CONNECTION_NETWORK_MAC, mac))
48
- if zigbee_mac := self.device.get("zigbee_mac_address"):
49
  connections.add((CONNECTION_ZIGBEE, zigbee_mac))
50
 
51
  # Set base device info
@@ -53,12 +65,12 @@
53
  configuration_url=configuration_url,
54
  identifiers={(DOMAIN, device_id)},
55
  connections=connections,
56
- manufacturer=self.device.get("vendor"),
57
- model=self.device.get("model"),
58
- model_id=self.device.get("model_id"),
59
  name=api.smile.name,
60
- sw_version=self.device.get("firmware"),
61
- hw_version=self.device.get("hardware"),
62
  )
63
 
64
  # Add extra info if not the gateway device
@@ -75,8 +87,10 @@
75
  def available(self) -> bool:
76
  """Return if entity is available."""
77
  return (
 
 
78
  self._dev_id in self.coordinator.data
79
- and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
80
  and super().available
81
  )
82
 
 
2
 
3
  from typing import override
4
 
5
+ from plugwise.constants import GwEntityData
6
 
7
  from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
8
  from homeassistant.helpers.device_registry import (
 
12
  )
13
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
14
 
15
+ from .const import (
16
+ AVAILABLE,
17
+ DOMAIN,
18
+ FIRMWARE,
19
+ HARDWARE,
20
+ MAC_ADDRESS,
21
+ MODEL,
22
+ MODEL_ID,
23
+ VENDOR,
24
+ ZIGBEE_MAC_ADDRESS,
25
+ )
26
+
27
+ # Upstream consts
28
  from .coordinator import PlugwiseDataUpdateCoordinator
29
 
30
 
 
55
 
56
  # Build connections set
57
  connections = set()
58
+ if mac := self.device.get(MAC_ADDRESS):
59
  connections.add((CONNECTION_NETWORK_MAC, mac))
60
+ if zigbee_mac := self.device.get(ZIGBEE_MAC_ADDRESS):
61
  connections.add((CONNECTION_ZIGBEE, zigbee_mac))
62
 
63
  # Set base device info
 
65
  configuration_url=configuration_url,
66
  identifiers={(DOMAIN, device_id)},
67
  connections=connections,
68
+ manufacturer=self.device.get(VENDOR),
69
+ model=self.device.get(MODEL),
70
+ model_id=self.device.get(MODEL_ID),
71
  name=api.smile.name,
72
+ sw_version=self.device.get(FIRMWARE),
73
+ hw_version=self.device.get(HARDWARE),
74
  )
75
 
76
  # Add extra info if not the gateway device
 
87
  def available(self) -> bool:
88
  """Return if entity is available."""
89
  return (
90
+ # Upstream: Do not change the AVAILABLE line below: some Plugwise devices and zones
91
+ # Upstream: do not provide their availability-status!
92
  self._dev_id in self.coordinator.data
93
+ and (self.device.get(AVAILABLE, True) is True)
94
  and super().available
95
  )
96
 
/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,13 +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
- "quality_scale": "platinum",
11
- "requirements": ["plugwise==1.11.4"],
12
  "zeroconf": ["_plugwise._tcp.local."]
13
  }
 
1
  {
2
  "domain": "plugwise",
3
+ "name": "Plugwise Beta",
4
  "codeowners": ["@CoMPaTech", "@bouwew"],
5
  "config_flow": true,
6
+ "documentation": "https://github.com/plugwise/plugwise-beta",
7
  "integration_type": "hub",
8
  "iot_class": "local_polling",
9
  "loggers": ["plugwise"],
10
+ "requirements": ["plugwise==1.12.0"],
11
+ "version": "0.65.0",
12
  "zeroconf": ["_plugwise._tcp.local."]
13
  }
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/number.py RENAMED
@@ -13,7 +13,17 @@
13
  from homeassistant.core import HomeAssistant, callback
14
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
15
 
16
- from .const import NumberType
 
 
 
 
 
 
 
 
 
 
17
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
18
  from .entity import PlugwiseEntity
19
  from .util import plugwise_command
@@ -28,24 +38,18 @@
28
  key: NumberType
29
 
30
 
 
31
  NUMBER_TYPES = (
32
  PlugwiseNumberEntityDescription(
33
- key="maximum_boiler_temperature",
34
- translation_key="maximum_boiler_temperature",
35
- device_class=NumberDeviceClass.TEMPERATURE,
36
- entity_category=EntityCategory.CONFIG,
37
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
38
- ),
39
- PlugwiseNumberEntityDescription(
40
- key="max_dhw_temperature",
41
- translation_key="max_dhw_temperature",
42
  device_class=NumberDeviceClass.TEMPERATURE,
43
  entity_category=EntityCategory.CONFIG,
44
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
45
  ),
46
  PlugwiseNumberEntityDescription(
47
- key="temperature_offset",
48
- translation_key="temperature_offset",
49
  device_class=NumberDeviceClass.TEMPERATURE,
50
  entity_category=EntityCategory.CONFIG,
51
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -58,7 +62,8 @@
58
  entry: PlugwiseConfigEntry,
59
  async_add_entities: AddConfigEntryEntitiesCallback,
60
  ) -> None:
61
- """Set up Plugwise number platform."""
 
62
  coordinator = entry.runtime_data
63
 
64
  @callback
@@ -67,12 +72,28 @@
67
  if not coordinator.new_devices:
68
  return
69
 
70
- async_add_entities(
71
- PlugwiseNumberEntity(coordinator, device_id, description)
72
- for device_id in coordinator.new_devices
73
- for description in NUMBER_TYPES
74
- if description.key in coordinator.data[device_id]
75
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  _add_entities()
78
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -91,28 +112,30 @@
91
  ) -> None:
92
  """Initiate Plugwise Number."""
93
  super().__init__(coordinator, device_id)
94
- self._attr_mode = NumberMode.BOX
95
- self._attr_native_max_value = self.device[description.key]["upper_bound"]
96
- self._attr_native_min_value = self.device[description.key]["lower_bound"]
97
- self._attr_unique_id = f"{device_id}-{description.key}"
98
  self.device_id = device_id
99
  self.entity_description = description
 
 
 
 
 
100
 
101
- native_step = self.device[description.key]["resolution"]
102
- if description.key != "temperature_offset":
103
  native_step = max(native_step, 0.5)
104
  self._attr_native_step = native_step
105
 
106
  @property
107
  @override
108
- def native_value(self) -> float:
109
  """Return the present setpoint value."""
110
- return self.device[self.entity_description.key]["setpoint"]
111
 
112
  @plugwise_command
113
  @override
114
  async def async_set_native_value(self, value: float) -> None:
115
  """Change to the new setpoint value."""
116
- await self.coordinator.api.set_number(
117
- self.device_id, self.entity_description.key, value
 
118
  )
 
13
  from homeassistant.core import HomeAssistant, callback
14
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
15
 
16
+ from .const import (
17
+ LOGGER,
18
+ LOWER_BOUND,
19
+ MAX_BOILER_TEMP,
20
+ RESOLUTION,
21
+ TEMPERATURE_OFFSET,
22
+ UPPER_BOUND,
23
+ NumberType,
24
+ )
25
+
26
+ # Upstream consts
27
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
28
  from .entity import PlugwiseEntity
29
  from .util import plugwise_command
 
38
  key: NumberType
39
 
40
 
41
+ # Upstream + is there a reason we didn't rename this one prefixed?
42
  NUMBER_TYPES = (
43
  PlugwiseNumberEntityDescription(
44
+ key=MAX_BOILER_TEMP,
45
+ translation_key=MAX_BOILER_TEMP,
 
 
 
 
 
 
 
46
  device_class=NumberDeviceClass.TEMPERATURE,
47
  entity_category=EntityCategory.CONFIG,
48
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
49
  ),
50
  PlugwiseNumberEntityDescription(
51
+ key=TEMPERATURE_OFFSET,
52
+ translation_key=TEMPERATURE_OFFSET,
53
  device_class=NumberDeviceClass.TEMPERATURE,
54
  entity_category=EntityCategory.CONFIG,
55
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 
62
  entry: PlugwiseConfigEntry,
63
  async_add_entities: AddConfigEntryEntitiesCallback,
64
  ) -> None:
65
+ """Set up Plugwise number platform from a config entry."""
66
+ # Upstream above to adhere to standard used
67
  coordinator = entry.runtime_data
68
 
69
  @callback
 
72
  if not coordinator.new_devices:
73
  return
74
 
75
+ # Upstream consts
76
+ # async_add_entities(
77
+ # PlugwiseNumberEntity(coordinator, device_id, description)
78
+ # for device_id in coordinator.new_devices
79
+ # for description in NUMBER_TYPES
80
+ # if description.key in coordinator.data.devices[device_id]
81
+ # )
82
+
83
+ # pw-beta alternative for debugging
84
+ entities: list[PlugwiseNumberEntity] = []
85
+ for device_id in coordinator.new_devices:
86
+ device = coordinator.data[device_id]
87
+ for description in NUMBER_TYPES:
88
+ if description.key in device:
89
+ entities.append(
90
+ PlugwiseNumberEntity(coordinator, device_id, description)
91
+ )
92
+ LOGGER.debug(
93
+ "Add %s %s number", device["name"], description.translation_key
94
+ )
95
+
96
+ async_add_entities(entities)
97
 
98
  _add_entities()
99
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
112
  ) -> None:
113
  """Initiate Plugwise Number."""
114
  super().__init__(coordinator, device_id)
 
 
 
 
115
  self.device_id = device_id
116
  self.entity_description = description
117
+ self._attr_unique_id = f"{device_id}-{description.key}"
118
+ self._attr_mode = NumberMode.BOX
119
+ ctrl = self.device.get(description.key, {})
120
+ self._attr_native_max_value = ctrl.get(UPPER_BOUND, 100.0) # Upstream const
121
+ self._attr_native_min_value = ctrl.get(LOWER_BOUND, 0.0) # Upstream const
122
 
123
+ native_step = ctrl.get(RESOLUTION, 0.5) # Upstream const
124
+ if description.key != TEMPERATURE_OFFSET: # Upstream const
125
  native_step = max(native_step, 0.5)
126
  self._attr_native_step = native_step
127
 
128
  @property
129
  @override
130
+ def native_value(self) -> float | None:
131
  """Return the present setpoint value."""
132
+ return self.device.get(self.entity_description.key, {}).get("setpoint")
133
 
134
  @plugwise_command
135
  @override
136
  async def async_set_native_value(self, value: float) -> None:
137
  """Change to the new setpoint value."""
138
+ await self.coordinator.api.set_number(self.device_id, self.entity_description.key, value)
139
+ LOGGER.debug(
140
+ "Setting %s to %s was successful", self.entity_description.key, value
141
  )
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/select.py RENAMED
@@ -9,15 +9,23 @@
9
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
10
 
11
  from .const import (
 
 
 
12
  LOCATION,
 
 
13
  SELECT_DHW_MODE,
14
  SELECT_GATEWAY_MODE,
15
  SELECT_REGULATION_MODE,
16
  SELECT_SCHEDULE,
17
  SELECT_ZONE_PROFILE,
 
18
  SelectOptionsType,
19
  SelectType,
20
  )
 
 
21
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
22
  from .entity import PlugwiseEntity
23
  from .util import plugwise_command
@@ -33,35 +41,36 @@
33
  options_key: SelectOptionsType
34
 
35
 
 
36
  SELECT_TYPES = (
37
  PlugwiseSelectEntityDescription(
38
  key=SELECT_SCHEDULE,
39
  translation_key=SELECT_SCHEDULE,
40
- options_key="available_schedules",
41
  ),
42
  PlugwiseSelectEntityDescription(
43
  key=SELECT_REGULATION_MODE,
44
  translation_key=SELECT_REGULATION_MODE,
45
  entity_category=EntityCategory.CONFIG,
46
- options_key="regulation_modes",
47
  ),
48
  PlugwiseSelectEntityDescription(
49
  key=SELECT_DHW_MODE,
50
  translation_key=SELECT_DHW_MODE,
51
  entity_category=EntityCategory.CONFIG,
52
- options_key="dhw_modes",
53
  ),
54
  PlugwiseSelectEntityDescription(
55
  key=SELECT_GATEWAY_MODE,
56
  translation_key=SELECT_GATEWAY_MODE,
57
  entity_category=EntityCategory.CONFIG,
58
- options_key="gateway_modes",
59
  ),
60
  PlugwiseSelectEntityDescription(
61
  key=SELECT_ZONE_PROFILE,
62
  translation_key=SELECT_ZONE_PROFILE,
63
  entity_category=EntityCategory.CONFIG,
64
- options_key="zone_profiles",
65
  ),
66
  )
67
 
@@ -71,7 +80,7 @@
71
  entry: PlugwiseConfigEntry,
72
  async_add_entities: AddConfigEntryEntitiesCallback,
73
  ) -> None:
74
- """Set up the Smile selector from a config entry."""
75
  coordinator = entry.runtime_data
76
 
77
  @callback
@@ -80,12 +89,27 @@
80
  if not coordinator.new_devices:
81
  return
82
 
83
- async_add_entities(
84
- PlugwiseSelectEntity(coordinator, device_id, description)
85
- for device_id in coordinator.new_devices
86
- for description in SELECT_TYPES
87
- if coordinator.data[device_id].get(description.options_key)
88
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  _add_entities()
91
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -118,23 +142,28 @@
118
  @override
119
  def current_option(self) -> str | None:
120
  """Return the selected entity option to represent the entity state."""
121
- return self.device[self.entity_description.key]
122
 
123
  @property
124
  @override
125
  def options(self) -> list[str]:
126
  """Return the available select-options."""
127
- return self.device[self.entity_description.options_key]
128
 
129
  @plugwise_command
130
  @override
131
  async def async_select_option(self, option: str) -> None:
132
  """Change to the selected entity option.
133
 
134
- The appliance ID (= device_id) is required for the dhw_mode select.
135
- The location ID is required for the thermostat schedule and zone_profile selects.
136
- STATE_ON is required for the thermostat schedule select.
137
  """
138
  await self.coordinator.api.set_select(
139
  self.entity_description.key, self._device_or_location, option, STATE_ON
140
  )
 
 
 
 
 
 
9
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
10
 
11
  from .const import (
12
+ AVAILABLE_SCHEDULES,
13
+ DHW_MODES,
14
+ GATEWAY_MODES,
15
  LOCATION,
16
+ LOGGER,
17
+ REGULATION_MODES,
18
  SELECT_DHW_MODE,
19
  SELECT_GATEWAY_MODE,
20
  SELECT_REGULATION_MODE,
21
  SELECT_SCHEDULE,
22
  SELECT_ZONE_PROFILE,
23
+ ZONE_PROFILES,
24
  SelectOptionsType,
25
  SelectType,
26
  )
27
+
28
+ # Upstream consts
29
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
30
  from .entity import PlugwiseEntity
31
  from .util import plugwise_command
 
41
  options_key: SelectOptionsType
42
 
43
 
44
+ # Upstream
45
  SELECT_TYPES = (
46
  PlugwiseSelectEntityDescription(
47
  key=SELECT_SCHEDULE,
48
  translation_key=SELECT_SCHEDULE,
49
+ options_key=AVAILABLE_SCHEDULES,
50
  ),
51
  PlugwiseSelectEntityDescription(
52
  key=SELECT_REGULATION_MODE,
53
  translation_key=SELECT_REGULATION_MODE,
54
  entity_category=EntityCategory.CONFIG,
55
+ options_key=REGULATION_MODES,
56
  ),
57
  PlugwiseSelectEntityDescription(
58
  key=SELECT_DHW_MODE,
59
  translation_key=SELECT_DHW_MODE,
60
  entity_category=EntityCategory.CONFIG,
61
+ options_key=DHW_MODES,
62
  ),
63
  PlugwiseSelectEntityDescription(
64
  key=SELECT_GATEWAY_MODE,
65
  translation_key=SELECT_GATEWAY_MODE,
66
  entity_category=EntityCategory.CONFIG,
67
+ options_key=GATEWAY_MODES,
68
  ),
69
  PlugwiseSelectEntityDescription(
70
  key=SELECT_ZONE_PROFILE,
71
  translation_key=SELECT_ZONE_PROFILE,
72
  entity_category=EntityCategory.CONFIG,
73
+ options_key=ZONE_PROFILES,
74
  ),
75
  )
76
 
 
80
  entry: PlugwiseConfigEntry,
81
  async_add_entities: AddConfigEntryEntitiesCallback,
82
  ) -> None:
83
+ """Set up Plugwise selector from a config entry."""
84
  coordinator = entry.runtime_data
85
 
86
  @callback
 
89
  if not coordinator.new_devices:
90
  return
91
 
92
+ # Upstream consts
93
+ # async_add_entities(
94
+ # PlugwiseSelectEntity(coordinator, device_id, description)
95
+ # for device_id in coordinator.new_devices
96
+ # for description in SELECT_TYPES
97
+ # if description.options_key in coordinator.data.devices[device_id]
98
+ # )
99
+ # pw-beta alternative for debugging
100
+ entities: list[PlugwiseSelectEntity] = []
101
+ for device_id in coordinator.new_devices:
102
+ device = coordinator.data[device_id]
103
+ for description in SELECT_TYPES:
104
+ if device.get(description.options_key):
105
+ entities.append(
106
+ PlugwiseSelectEntity(coordinator, device_id, description)
107
+ )
108
+ LOGGER.debug(
109
+ "Add %s %s selector", device["name"], description.translation_key
110
+ )
111
+
112
+ async_add_entities(entities)
113
 
114
  _add_entities()
115
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
142
  @override
143
  def current_option(self) -> str | None:
144
  """Return the selected entity option to represent the entity state."""
145
+ return self.device.get(self.entity_description.key)
146
 
147
  @property
148
  @override
149
  def options(self) -> list[str]:
150
  """Return the available select-options."""
151
+ return self.device.get(self.entity_description.options_key, [])
152
 
153
  @plugwise_command
154
  @override
155
  async def async_select_option(self, option: str) -> None:
156
  """Change to the selected entity option.
157
 
158
+ Appliance ID (= device_id) is required for the dhw_mode select
159
+ Locattion ID is required for the thermostat-schedule and zone_profile selects.
160
+ STATE_ON is required for the thermostat-schedule select.
161
  """
162
  await self.coordinator.api.set_select(
163
  self.entity_description.key, self._device_or_location, option, STATE_ON
164
  )
165
+ LOGGER.debug(
166
+ "Set %s to %s was successful",
167
+ self.entity_description.key,
168
+ option,
169
+ )
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/sensor.py RENAMED
@@ -12,6 +12,7 @@
12
  SensorStateClass,
13
  )
14
  from homeassistant.const import (
 
15
  LIGHT_LUX,
16
  PERCENTAGE,
17
  EntityCategory,
@@ -26,6 +27,57 @@
26
  from homeassistant.core import HomeAssistant, callback
27
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
30
  from .entity import PlugwiseEntity
31
 
@@ -40,299 +92,301 @@
40
  key: SensorType
41
 
42
 
43
- SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
 
44
  PlugwiseSensorEntityDescription(
45
- key="setpoint",
46
- translation_key="setpoint",
47
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
48
  device_class=SensorDeviceClass.TEMPERATURE,
49
  state_class=SensorStateClass.MEASUREMENT,
50
  ),
51
  PlugwiseSensorEntityDescription(
52
- key="setpoint_high",
53
  translation_key="cooling_setpoint",
54
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
55
  device_class=SensorDeviceClass.TEMPERATURE,
56
  state_class=SensorStateClass.MEASUREMENT,
57
  ),
58
  PlugwiseSensorEntityDescription(
59
- key="setpoint_low",
60
  translation_key="heating_setpoint",
61
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
62
  device_class=SensorDeviceClass.TEMPERATURE,
63
  state_class=SensorStateClass.MEASUREMENT,
64
  ),
65
  PlugwiseSensorEntityDescription(
66
- key="temperature",
67
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
68
  device_class=SensorDeviceClass.TEMPERATURE,
69
  state_class=SensorStateClass.MEASUREMENT,
70
  ),
71
  PlugwiseSensorEntityDescription(
72
- key="intended_boiler_temperature",
73
- translation_key="intended_boiler_temperature",
74
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
75
  device_class=SensorDeviceClass.TEMPERATURE,
76
  entity_category=EntityCategory.DIAGNOSTIC,
77
  state_class=SensorStateClass.MEASUREMENT,
78
  ),
79
  PlugwiseSensorEntityDescription(
80
- key="temperature_difference",
81
- translation_key="temperature_difference",
82
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
83
  device_class=SensorDeviceClass.TEMPERATURE,
84
  entity_category=EntityCategory.DIAGNOSTIC,
85
  state_class=SensorStateClass.MEASUREMENT,
86
  ),
87
  PlugwiseSensorEntityDescription(
88
- key="outdoor_temperature",
89
- translation_key="outdoor_temperature",
90
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
91
  device_class=SensorDeviceClass.TEMPERATURE,
92
  entity_category=EntityCategory.DIAGNOSTIC,
93
  state_class=SensorStateClass.MEASUREMENT,
 
94
  ),
95
  PlugwiseSensorEntityDescription(
96
- key="outdoor_air_temperature",
97
- translation_key="outdoor_air_temperature",
98
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
99
  device_class=SensorDeviceClass.TEMPERATURE,
100
  entity_category=EntityCategory.DIAGNOSTIC,
101
  state_class=SensorStateClass.MEASUREMENT,
102
  ),
103
  PlugwiseSensorEntityDescription(
104
- key="water_temperature",
105
- translation_key="water_temperature",
106
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
107
  device_class=SensorDeviceClass.TEMPERATURE,
108
  entity_category=EntityCategory.DIAGNOSTIC,
109
  state_class=SensorStateClass.MEASUREMENT,
110
  ),
111
  PlugwiseSensorEntityDescription(
112
- key="return_temperature",
113
- translation_key="return_temperature",
114
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
115
  device_class=SensorDeviceClass.TEMPERATURE,
116
  entity_category=EntityCategory.DIAGNOSTIC,
117
  state_class=SensorStateClass.MEASUREMENT,
118
  ),
119
  PlugwiseSensorEntityDescription(
120
- key="electricity_consumed",
121
- translation_key="electricity_consumed",
122
  native_unit_of_measurement=UnitOfPower.WATT,
123
  device_class=SensorDeviceClass.POWER,
124
  state_class=SensorStateClass.MEASUREMENT,
125
  ),
126
  PlugwiseSensorEntityDescription(
127
- key="electricity_produced",
128
- translation_key="electricity_produced",
129
  native_unit_of_measurement=UnitOfPower.WATT,
130
  device_class=SensorDeviceClass.POWER,
131
  state_class=SensorStateClass.MEASUREMENT,
132
  entity_registry_enabled_default=False,
133
  ),
134
  PlugwiseSensorEntityDescription(
135
- key="electricity_consumed_interval",
136
- translation_key="electricity_consumed_interval",
137
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
138
  device_class=SensorDeviceClass.ENERGY,
139
  state_class=SensorStateClass.TOTAL,
140
  ),
141
  PlugwiseSensorEntityDescription(
142
- key="electricity_consumed_peak_interval",
143
- translation_key="electricity_consumed_peak_interval",
144
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
145
  device_class=SensorDeviceClass.ENERGY,
146
  state_class=SensorStateClass.TOTAL,
147
  ),
148
  PlugwiseSensorEntityDescription(
149
- key="electricity_consumed_off_peak_interval",
150
- translation_key="electricity_consumed_off_peak_interval",
151
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
152
  device_class=SensorDeviceClass.ENERGY,
153
  state_class=SensorStateClass.TOTAL,
154
  ),
155
  PlugwiseSensorEntityDescription(
156
- key="electricity_produced_interval",
157
- translation_key="electricity_produced_interval",
158
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
159
  device_class=SensorDeviceClass.ENERGY,
160
  state_class=SensorStateClass.TOTAL,
161
  entity_registry_enabled_default=False,
162
  ),
163
  PlugwiseSensorEntityDescription(
164
- key="electricity_produced_peak_interval",
165
- translation_key="electricity_produced_peak_interval",
166
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
167
  device_class=SensorDeviceClass.ENERGY,
168
  state_class=SensorStateClass.TOTAL,
169
  ),
170
  PlugwiseSensorEntityDescription(
171
- key="electricity_produced_off_peak_interval",
172
- translation_key="electricity_produced_off_peak_interval",
173
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
174
  device_class=SensorDeviceClass.ENERGY,
175
  state_class=SensorStateClass.TOTAL,
176
  ),
177
  PlugwiseSensorEntityDescription(
178
- key="electricity_consumed_point",
179
- translation_key="electricity_consumed_point",
180
  device_class=SensorDeviceClass.POWER,
181
  native_unit_of_measurement=UnitOfPower.WATT,
182
  state_class=SensorStateClass.MEASUREMENT,
183
  ),
184
  PlugwiseSensorEntityDescription(
185
- key="electricity_consumed_off_peak_point",
186
- translation_key="electricity_consumed_off_peak_point",
187
  native_unit_of_measurement=UnitOfPower.WATT,
188
  device_class=SensorDeviceClass.POWER,
189
  state_class=SensorStateClass.MEASUREMENT,
190
  ),
191
  PlugwiseSensorEntityDescription(
192
- key="electricity_consumed_peak_point",
193
- translation_key="electricity_consumed_peak_point",
194
  native_unit_of_measurement=UnitOfPower.WATT,
195
  device_class=SensorDeviceClass.POWER,
196
  state_class=SensorStateClass.MEASUREMENT,
197
  ),
198
  PlugwiseSensorEntityDescription(
199
- key="electricity_consumed_off_peak_cumulative",
200
- translation_key="electricity_consumed_off_peak_cumulative",
201
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
202
  device_class=SensorDeviceClass.ENERGY,
203
  state_class=SensorStateClass.TOTAL_INCREASING,
204
  ),
205
  PlugwiseSensorEntityDescription(
206
- key="electricity_consumed_peak_cumulative",
207
- translation_key="electricity_consumed_peak_cumulative",
208
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
209
  device_class=SensorDeviceClass.ENERGY,
210
  state_class=SensorStateClass.TOTAL_INCREASING,
211
  ),
212
  PlugwiseSensorEntityDescription(
213
- key="electricity_produced_point",
214
- translation_key="electricity_produced_point",
215
  device_class=SensorDeviceClass.POWER,
216
  native_unit_of_measurement=UnitOfPower.WATT,
217
  state_class=SensorStateClass.MEASUREMENT,
218
  ),
219
  PlugwiseSensorEntityDescription(
220
- key="electricity_produced_off_peak_point",
221
- translation_key="electricity_produced_off_peak_point",
222
  native_unit_of_measurement=UnitOfPower.WATT,
223
  device_class=SensorDeviceClass.POWER,
224
  state_class=SensorStateClass.MEASUREMENT,
225
  ),
226
  PlugwiseSensorEntityDescription(
227
- key="electricity_produced_peak_point",
228
- translation_key="electricity_produced_peak_point",
229
  native_unit_of_measurement=UnitOfPower.WATT,
230
  device_class=SensorDeviceClass.POWER,
231
  state_class=SensorStateClass.MEASUREMENT,
232
  ),
233
  PlugwiseSensorEntityDescription(
234
- key="electricity_produced_off_peak_cumulative",
235
- translation_key="electricity_produced_off_peak_cumulative",
236
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
237
  device_class=SensorDeviceClass.ENERGY,
238
  state_class=SensorStateClass.TOTAL_INCREASING,
239
  ),
240
  PlugwiseSensorEntityDescription(
241
- key="electricity_produced_peak_cumulative",
242
- translation_key="electricity_produced_peak_cumulative",
243
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
244
  device_class=SensorDeviceClass.ENERGY,
245
  state_class=SensorStateClass.TOTAL_INCREASING,
246
  ),
247
  PlugwiseSensorEntityDescription(
248
- key="electricity_phase_one_consumed",
249
- translation_key="electricity_phase_one_consumed",
250
  device_class=SensorDeviceClass.POWER,
251
  native_unit_of_measurement=UnitOfPower.WATT,
252
  state_class=SensorStateClass.MEASUREMENT,
253
  ),
254
  PlugwiseSensorEntityDescription(
255
- key="electricity_phase_two_consumed",
256
- translation_key="electricity_phase_two_consumed",
257
  device_class=SensorDeviceClass.POWER,
258
  native_unit_of_measurement=UnitOfPower.WATT,
259
  state_class=SensorStateClass.MEASUREMENT,
260
  ),
261
  PlugwiseSensorEntityDescription(
262
- key="electricity_phase_three_consumed",
263
- translation_key="electricity_phase_three_consumed",
264
  device_class=SensorDeviceClass.POWER,
265
  native_unit_of_measurement=UnitOfPower.WATT,
266
  state_class=SensorStateClass.MEASUREMENT,
267
  ),
268
  PlugwiseSensorEntityDescription(
269
- key="electricity_phase_one_produced",
270
- translation_key="electricity_phase_one_produced",
271
  device_class=SensorDeviceClass.POWER,
272
  native_unit_of_measurement=UnitOfPower.WATT,
273
  state_class=SensorStateClass.MEASUREMENT,
274
  ),
275
  PlugwiseSensorEntityDescription(
276
- key="electricity_phase_two_produced",
277
- translation_key="electricity_phase_two_produced",
278
  device_class=SensorDeviceClass.POWER,
279
  native_unit_of_measurement=UnitOfPower.WATT,
280
  state_class=SensorStateClass.MEASUREMENT,
281
  ),
282
  PlugwiseSensorEntityDescription(
283
- key="electricity_phase_three_produced",
284
- translation_key="electricity_phase_three_produced",
285
  device_class=SensorDeviceClass.POWER,
286
  native_unit_of_measurement=UnitOfPower.WATT,
287
  state_class=SensorStateClass.MEASUREMENT,
288
  ),
289
  PlugwiseSensorEntityDescription(
290
- key="voltage_phase_one",
291
- translation_key="voltage_phase_one",
292
  device_class=SensorDeviceClass.VOLTAGE,
293
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
294
  state_class=SensorStateClass.MEASUREMENT,
295
  entity_registry_enabled_default=False,
296
  ),
297
  PlugwiseSensorEntityDescription(
298
- key="voltage_phase_two",
299
- translation_key="voltage_phase_two",
300
  device_class=SensorDeviceClass.VOLTAGE,
301
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
302
  state_class=SensorStateClass.MEASUREMENT,
303
  entity_registry_enabled_default=False,
304
  ),
305
  PlugwiseSensorEntityDescription(
306
- key="voltage_phase_three",
307
- translation_key="voltage_phase_three",
308
  device_class=SensorDeviceClass.VOLTAGE,
309
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
310
  state_class=SensorStateClass.MEASUREMENT,
311
  entity_registry_enabled_default=False,
312
  ),
313
  PlugwiseSensorEntityDescription(
314
- key="gas_consumed_interval",
315
- translation_key="gas_consumed_interval",
316
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
317
  state_class=SensorStateClass.MEASUREMENT,
318
  ),
319
  PlugwiseSensorEntityDescription(
320
- key="gas_consumed_cumulative",
321
- translation_key="gas_consumed_cumulative",
322
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
323
  device_class=SensorDeviceClass.GAS,
324
  state_class=SensorStateClass.TOTAL,
325
  ),
326
  PlugwiseSensorEntityDescription(
327
- key="net_electricity_point",
328
- translation_key="net_electricity_point",
329
  native_unit_of_measurement=UnitOfPower.WATT,
330
  device_class=SensorDeviceClass.POWER,
331
  state_class=SensorStateClass.MEASUREMENT,
332
  ),
333
  PlugwiseSensorEntityDescription(
334
- key="net_electricity_cumulative",
335
- translation_key="net_electricity_cumulative",
336
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
337
  device_class=SensorDeviceClass.ENERGY,
338
  state_class=SensorStateClass.TOTAL,
@@ -352,22 +406,22 @@
352
  state_class=SensorStateClass.MEASUREMENT,
353
  ),
354
  PlugwiseSensorEntityDescription(
355
- key="modulation_level",
356
- translation_key="modulation_level",
357
  native_unit_of_measurement=PERCENTAGE,
358
  entity_category=EntityCategory.DIAGNOSTIC,
359
  state_class=SensorStateClass.MEASUREMENT,
360
  ),
361
  PlugwiseSensorEntityDescription(
362
- key="valve_position",
363
- translation_key="valve_position",
364
  native_unit_of_measurement=PERCENTAGE,
365
  entity_category=EntityCategory.DIAGNOSTIC,
366
  state_class=SensorStateClass.MEASUREMENT,
367
  ),
368
  PlugwiseSensorEntityDescription(
369
- key="water_pressure",
370
- translation_key="water_pressure",
371
  native_unit_of_measurement=UnitOfPressure.BAR,
372
  device_class=SensorDeviceClass.PRESSURE,
373
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -380,16 +434,16 @@
380
  state_class=SensorStateClass.MEASUREMENT,
381
  ),
382
  PlugwiseSensorEntityDescription(
383
- key="dhw_temperature",
384
- translation_key="dhw_temperature",
385
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
386
  device_class=SensorDeviceClass.TEMPERATURE,
387
  entity_category=EntityCategory.DIAGNOSTIC,
388
  state_class=SensorStateClass.MEASUREMENT,
389
  ),
390
  PlugwiseSensorEntityDescription(
391
- key="domestic_hot_water_setpoint",
392
- translation_key="domestic_hot_water_setpoint",
393
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
394
  device_class=SensorDeviceClass.TEMPERATURE,
395
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -403,7 +457,8 @@
403
  entry: PlugwiseConfigEntry,
404
  async_add_entities: AddConfigEntryEntitiesCallback,
405
  ) -> None:
406
- """Set up the Smile sensors from a config entry."""
 
407
  coordinator = entry.runtime_data
408
 
409
  @callback
@@ -412,13 +467,29 @@
412
  if not coordinator.new_devices:
413
  return
414
 
415
- async_add_entities(
416
- PlugwiseSensorEntity(coordinator, device_id, description)
417
- for device_id in coordinator.new_devices
418
- if (sensors := coordinator.data[device_id].get("sensors"))
419
- for description in SENSORS
420
- if description.key in sensors
421
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
  _add_entities()
424
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -437,11 +508,11 @@
437
  ) -> None:
438
  """Initialise the sensor."""
439
  super().__init__(coordinator, device_id)
440
- self._attr_unique_id = f"{device_id}-{description.key}"
441
  self.entity_description = description
 
442
 
443
  @property
444
  @override
445
- def native_value(self) -> int | float:
446
  """Return the value reported by the sensor."""
447
- return self.device["sensors"][self.entity_description.key]
 
12
  SensorStateClass,
13
  )
14
  from homeassistant.const import (
15
+ ATTR_TEMPERATURE, # Upstream
16
  LIGHT_LUX,
17
  PERCENTAGE,
18
  EntityCategory,
 
27
  from homeassistant.core import HomeAssistant, callback
28
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
29
 
30
+ from .const import (
31
+ DHW_SETPOINT,
32
+ DHW_TEMP,
33
+ EL_CONS_INTERVAL,
34
+ EL_CONS_OP_CUMULATIVE,
35
+ EL_CONS_OP_INTERVAL,
36
+ EL_CONS_OP_POINT,
37
+ EL_CONS_P_CUMULATIVE,
38
+ EL_CONS_P_INTERVAL,
39
+ EL_CONS_P_POINT,
40
+ EL_CONS_POINT,
41
+ EL_CONSUMED,
42
+ EL_PH1_CONSUMED,
43
+ EL_PH1_PRODUCED,
44
+ EL_PH2_CONSUMED,
45
+ EL_PH2_PRODUCED,
46
+ EL_PH3_CONSUMED,
47
+ EL_PH3_PRODUCED,
48
+ EL_PROD_INTERVAL,
49
+ EL_PROD_OP_CUMULATIVE,
50
+ EL_PROD_OP_INTERVAL,
51
+ EL_PROD_OP_POINT,
52
+ EL_PROD_P_CUMULATIVE,
53
+ EL_PROD_P_INTERVAL,
54
+ EL_PROD_P_POINT,
55
+ EL_PROD_POINT,
56
+ EL_PRODUCED,
57
+ GAS_CONS_CUMULATIVE,
58
+ GAS_CONS_INTERVAL,
59
+ INTENDED_BOILER_TEMP,
60
+ LOGGER, # pw-beta
61
+ MOD_LEVEL,
62
+ NET_EL_CUMULATIVE,
63
+ NET_EL_POINT,
64
+ OUTDOOR_AIR_TEMP,
65
+ OUTDOOR_TEMP,
66
+ RETURN_TEMP,
67
+ SENSORS,
68
+ TARGET_TEMP,
69
+ TARGET_TEMP_HIGH,
70
+ TARGET_TEMP_LOW,
71
+ TEMP_DIFF,
72
+ VALVE_POS,
73
+ VOLTAGE_PH1,
74
+ VOLTAGE_PH2,
75
+ VOLTAGE_PH3,
76
+ WATER_PRESSURE,
77
+ WATER_TEMP,
78
+ )
79
+
80
+ # Upstream consts
81
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
82
  from .entity import PlugwiseEntity
83
 
 
92
  key: SensorType
93
 
94
 
95
+ # Upstream consts
96
+ PLUGWISE_SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
97
  PlugwiseSensorEntityDescription(
98
+ key=TARGET_TEMP,
99
+ translation_key=TARGET_TEMP,
100
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
101
  device_class=SensorDeviceClass.TEMPERATURE,
102
  state_class=SensorStateClass.MEASUREMENT,
103
  ),
104
  PlugwiseSensorEntityDescription(
105
+ key=TARGET_TEMP_HIGH,
106
  translation_key="cooling_setpoint",
107
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
108
  device_class=SensorDeviceClass.TEMPERATURE,
109
  state_class=SensorStateClass.MEASUREMENT,
110
  ),
111
  PlugwiseSensorEntityDescription(
112
+ key=TARGET_TEMP_LOW,
113
  translation_key="heating_setpoint",
114
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
115
  device_class=SensorDeviceClass.TEMPERATURE,
116
  state_class=SensorStateClass.MEASUREMENT,
117
  ),
118
  PlugwiseSensorEntityDescription(
119
+ key=ATTR_TEMPERATURE,
120
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
121
  device_class=SensorDeviceClass.TEMPERATURE,
122
  state_class=SensorStateClass.MEASUREMENT,
123
  ),
124
  PlugwiseSensorEntityDescription(
125
+ key=INTENDED_BOILER_TEMP,
126
+ translation_key=INTENDED_BOILER_TEMP,
127
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
128
  device_class=SensorDeviceClass.TEMPERATURE,
129
  entity_category=EntityCategory.DIAGNOSTIC,
130
  state_class=SensorStateClass.MEASUREMENT,
131
  ),
132
  PlugwiseSensorEntityDescription(
133
+ key=TEMP_DIFF,
134
+ translation_key=TEMP_DIFF,
135
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
136
  device_class=SensorDeviceClass.TEMPERATURE,
137
  entity_category=EntityCategory.DIAGNOSTIC,
138
  state_class=SensorStateClass.MEASUREMENT,
139
  ),
140
  PlugwiseSensorEntityDescription(
141
+ key=OUTDOOR_TEMP,
142
+ translation_key=OUTDOOR_TEMP,
143
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
144
  device_class=SensorDeviceClass.TEMPERATURE,
145
  entity_category=EntityCategory.DIAGNOSTIC,
146
  state_class=SensorStateClass.MEASUREMENT,
147
+ suggested_display_precision=1,
148
  ),
149
  PlugwiseSensorEntityDescription(
150
+ key=OUTDOOR_AIR_TEMP,
151
+ translation_key=OUTDOOR_AIR_TEMP,
152
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
153
  device_class=SensorDeviceClass.TEMPERATURE,
154
  entity_category=EntityCategory.DIAGNOSTIC,
155
  state_class=SensorStateClass.MEASUREMENT,
156
  ),
157
  PlugwiseSensorEntityDescription(
158
+ key=WATER_TEMP,
159
+ translation_key=WATER_TEMP,
160
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
161
  device_class=SensorDeviceClass.TEMPERATURE,
162
  entity_category=EntityCategory.DIAGNOSTIC,
163
  state_class=SensorStateClass.MEASUREMENT,
164
  ),
165
  PlugwiseSensorEntityDescription(
166
+ key=RETURN_TEMP,
167
+ translation_key=RETURN_TEMP,
168
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
169
  device_class=SensorDeviceClass.TEMPERATURE,
170
  entity_category=EntityCategory.DIAGNOSTIC,
171
  state_class=SensorStateClass.MEASUREMENT,
172
  ),
173
  PlugwiseSensorEntityDescription(
174
+ key=EL_CONSUMED,
175
+ translation_key=EL_CONSUMED,
176
  native_unit_of_measurement=UnitOfPower.WATT,
177
  device_class=SensorDeviceClass.POWER,
178
  state_class=SensorStateClass.MEASUREMENT,
179
  ),
180
  PlugwiseSensorEntityDescription(
181
+ key=EL_PRODUCED,
182
+ translation_key=EL_PRODUCED,
183
  native_unit_of_measurement=UnitOfPower.WATT,
184
  device_class=SensorDeviceClass.POWER,
185
  state_class=SensorStateClass.MEASUREMENT,
186
  entity_registry_enabled_default=False,
187
  ),
188
  PlugwiseSensorEntityDescription(
189
+ key=EL_CONS_INTERVAL,
190
+ translation_key=EL_CONS_INTERVAL,
191
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
192
  device_class=SensorDeviceClass.ENERGY,
193
  state_class=SensorStateClass.TOTAL,
194
  ),
195
  PlugwiseSensorEntityDescription(
196
+ key=EL_CONS_P_INTERVAL,
197
+ translation_key=EL_CONS_P_INTERVAL,
198
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
199
  device_class=SensorDeviceClass.ENERGY,
200
  state_class=SensorStateClass.TOTAL,
201
  ),
202
  PlugwiseSensorEntityDescription(
203
+ key=EL_CONS_OP_INTERVAL,
204
+ translation_key=EL_CONS_OP_INTERVAL,
205
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
206
  device_class=SensorDeviceClass.ENERGY,
207
  state_class=SensorStateClass.TOTAL,
208
  ),
209
  PlugwiseSensorEntityDescription(
210
+ key=EL_PROD_INTERVAL,
211
+ translation_key=EL_PROD_INTERVAL,
212
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
213
  device_class=SensorDeviceClass.ENERGY,
214
  state_class=SensorStateClass.TOTAL,
215
  entity_registry_enabled_default=False,
216
  ),
217
  PlugwiseSensorEntityDescription(
218
+ key=EL_PROD_P_INTERVAL,
219
+ translation_key=EL_PROD_P_INTERVAL,
220
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
221
  device_class=SensorDeviceClass.ENERGY,
222
  state_class=SensorStateClass.TOTAL,
223
  ),
224
  PlugwiseSensorEntityDescription(
225
+ key=EL_PROD_OP_INTERVAL,
226
+ translation_key=EL_PROD_OP_INTERVAL,
227
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
228
  device_class=SensorDeviceClass.ENERGY,
229
  state_class=SensorStateClass.TOTAL,
230
  ),
231
  PlugwiseSensorEntityDescription(
232
+ key=EL_CONS_POINT,
233
+ translation_key=EL_CONS_POINT,
234
  device_class=SensorDeviceClass.POWER,
235
  native_unit_of_measurement=UnitOfPower.WATT,
236
  state_class=SensorStateClass.MEASUREMENT,
237
  ),
238
  PlugwiseSensorEntityDescription(
239
+ key=EL_CONS_OP_POINT,
240
+ translation_key=EL_CONS_OP_POINT,
241
  native_unit_of_measurement=UnitOfPower.WATT,
242
  device_class=SensorDeviceClass.POWER,
243
  state_class=SensorStateClass.MEASUREMENT,
244
  ),
245
  PlugwiseSensorEntityDescription(
246
+ key=EL_CONS_P_POINT,
247
+ translation_key=EL_CONS_P_POINT,
248
  native_unit_of_measurement=UnitOfPower.WATT,
249
  device_class=SensorDeviceClass.POWER,
250
  state_class=SensorStateClass.MEASUREMENT,
251
  ),
252
  PlugwiseSensorEntityDescription(
253
+ key=EL_CONS_OP_CUMULATIVE,
254
+ translation_key=EL_CONS_OP_CUMULATIVE,
255
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
256
  device_class=SensorDeviceClass.ENERGY,
257
  state_class=SensorStateClass.TOTAL_INCREASING,
258
  ),
259
  PlugwiseSensorEntityDescription(
260
+ key=EL_CONS_P_CUMULATIVE,
261
+ translation_key=EL_CONS_P_CUMULATIVE,
262
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
263
  device_class=SensorDeviceClass.ENERGY,
264
  state_class=SensorStateClass.TOTAL_INCREASING,
265
  ),
266
  PlugwiseSensorEntityDescription(
267
+ key=EL_PROD_POINT,
268
+ translation_key=EL_PROD_POINT,
269
  device_class=SensorDeviceClass.POWER,
270
  native_unit_of_measurement=UnitOfPower.WATT,
271
  state_class=SensorStateClass.MEASUREMENT,
272
  ),
273
  PlugwiseSensorEntityDescription(
274
+ key=EL_PROD_OP_POINT,
275
+ translation_key=EL_PROD_OP_POINT,
276
  native_unit_of_measurement=UnitOfPower.WATT,
277
  device_class=SensorDeviceClass.POWER,
278
  state_class=SensorStateClass.MEASUREMENT,
279
  ),
280
  PlugwiseSensorEntityDescription(
281
+ key=EL_PROD_P_POINT,
282
+ translation_key=EL_PROD_P_POINT,
283
  native_unit_of_measurement=UnitOfPower.WATT,
284
  device_class=SensorDeviceClass.POWER,
285
  state_class=SensorStateClass.MEASUREMENT,
286
  ),
287
  PlugwiseSensorEntityDescription(
288
+ key=EL_PROD_OP_CUMULATIVE,
289
+ translation_key=EL_PROD_OP_CUMULATIVE,
290
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
291
  device_class=SensorDeviceClass.ENERGY,
292
  state_class=SensorStateClass.TOTAL_INCREASING,
293
  ),
294
  PlugwiseSensorEntityDescription(
295
+ key=EL_PROD_P_CUMULATIVE,
296
+ translation_key=EL_PROD_P_CUMULATIVE,
297
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
298
  device_class=SensorDeviceClass.ENERGY,
299
  state_class=SensorStateClass.TOTAL_INCREASING,
300
  ),
301
  PlugwiseSensorEntityDescription(
302
+ key=EL_PH1_CONSUMED,
303
+ translation_key=EL_PH1_CONSUMED,
304
  device_class=SensorDeviceClass.POWER,
305
  native_unit_of_measurement=UnitOfPower.WATT,
306
  state_class=SensorStateClass.MEASUREMENT,
307
  ),
308
  PlugwiseSensorEntityDescription(
309
+ key=EL_PH2_CONSUMED,
310
+ translation_key=EL_PH2_CONSUMED,
311
  device_class=SensorDeviceClass.POWER,
312
  native_unit_of_measurement=UnitOfPower.WATT,
313
  state_class=SensorStateClass.MEASUREMENT,
314
  ),
315
  PlugwiseSensorEntityDescription(
316
+ key=EL_PH3_CONSUMED,
317
+ translation_key=EL_PH3_CONSUMED,
318
  device_class=SensorDeviceClass.POWER,
319
  native_unit_of_measurement=UnitOfPower.WATT,
320
  state_class=SensorStateClass.MEASUREMENT,
321
  ),
322
  PlugwiseSensorEntityDescription(
323
+ key=EL_PH1_PRODUCED,
324
+ translation_key=EL_PH1_PRODUCED,
325
  device_class=SensorDeviceClass.POWER,
326
  native_unit_of_measurement=UnitOfPower.WATT,
327
  state_class=SensorStateClass.MEASUREMENT,
328
  ),
329
  PlugwiseSensorEntityDescription(
330
+ key=EL_PH2_PRODUCED,
331
+ translation_key=EL_PH2_PRODUCED,
332
  device_class=SensorDeviceClass.POWER,
333
  native_unit_of_measurement=UnitOfPower.WATT,
334
  state_class=SensorStateClass.MEASUREMENT,
335
  ),
336
  PlugwiseSensorEntityDescription(
337
+ key=EL_PH3_PRODUCED,
338
+ translation_key=EL_PH3_PRODUCED,
339
  device_class=SensorDeviceClass.POWER,
340
  native_unit_of_measurement=UnitOfPower.WATT,
341
  state_class=SensorStateClass.MEASUREMENT,
342
  ),
343
  PlugwiseSensorEntityDescription(
344
+ key=VOLTAGE_PH1,
345
+ translation_key=VOLTAGE_PH1,
346
  device_class=SensorDeviceClass.VOLTAGE,
347
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
348
  state_class=SensorStateClass.MEASUREMENT,
349
  entity_registry_enabled_default=False,
350
  ),
351
  PlugwiseSensorEntityDescription(
352
+ key=VOLTAGE_PH2,
353
+ translation_key=VOLTAGE_PH2,
354
  device_class=SensorDeviceClass.VOLTAGE,
355
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
356
  state_class=SensorStateClass.MEASUREMENT,
357
  entity_registry_enabled_default=False,
358
  ),
359
  PlugwiseSensorEntityDescription(
360
+ key=VOLTAGE_PH3,
361
+ translation_key=VOLTAGE_PH3,
362
  device_class=SensorDeviceClass.VOLTAGE,
363
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
364
  state_class=SensorStateClass.MEASUREMENT,
365
  entity_registry_enabled_default=False,
366
  ),
367
  PlugwiseSensorEntityDescription(
368
+ key=GAS_CONS_INTERVAL,
369
+ translation_key=GAS_CONS_INTERVAL,
370
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
371
  state_class=SensorStateClass.MEASUREMENT,
372
  ),
373
  PlugwiseSensorEntityDescription(
374
+ key=GAS_CONS_CUMULATIVE,
375
+ translation_key=GAS_CONS_CUMULATIVE,
376
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
377
  device_class=SensorDeviceClass.GAS,
378
  state_class=SensorStateClass.TOTAL,
379
  ),
380
  PlugwiseSensorEntityDescription(
381
+ key=NET_EL_POINT,
382
+ translation_key=NET_EL_POINT,
383
  native_unit_of_measurement=UnitOfPower.WATT,
384
  device_class=SensorDeviceClass.POWER,
385
  state_class=SensorStateClass.MEASUREMENT,
386
  ),
387
  PlugwiseSensorEntityDescription(
388
+ key=NET_EL_CUMULATIVE,
389
+ translation_key=NET_EL_CUMULATIVE,
390
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
391
  device_class=SensorDeviceClass.ENERGY,
392
  state_class=SensorStateClass.TOTAL,
 
406
  state_class=SensorStateClass.MEASUREMENT,
407
  ),
408
  PlugwiseSensorEntityDescription(
409
+ key=MOD_LEVEL,
410
+ translation_key=MOD_LEVEL,
411
  native_unit_of_measurement=PERCENTAGE,
412
  entity_category=EntityCategory.DIAGNOSTIC,
413
  state_class=SensorStateClass.MEASUREMENT,
414
  ),
415
  PlugwiseSensorEntityDescription(
416
+ key=VALVE_POS,
417
+ translation_key=VALVE_POS,
418
  native_unit_of_measurement=PERCENTAGE,
419
  entity_category=EntityCategory.DIAGNOSTIC,
420
  state_class=SensorStateClass.MEASUREMENT,
421
  ),
422
  PlugwiseSensorEntityDescription(
423
+ key=WATER_PRESSURE,
424
+ translation_key=WATER_PRESSURE,
425
  native_unit_of_measurement=UnitOfPressure.BAR,
426
  device_class=SensorDeviceClass.PRESSURE,
427
  entity_category=EntityCategory.DIAGNOSTIC,
 
434
  state_class=SensorStateClass.MEASUREMENT,
435
  ),
436
  PlugwiseSensorEntityDescription(
437
+ key=DHW_TEMP,
438
+ translation_key=DHW_TEMP,
439
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
440
  device_class=SensorDeviceClass.TEMPERATURE,
441
  entity_category=EntityCategory.DIAGNOSTIC,
442
  state_class=SensorStateClass.MEASUREMENT,
443
  ),
444
  PlugwiseSensorEntityDescription(
445
+ key=DHW_SETPOINT,
446
+ translation_key=DHW_SETPOINT,
447
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
448
  device_class=SensorDeviceClass.TEMPERATURE,
449
  entity_category=EntityCategory.DIAGNOSTIC,
 
457
  entry: PlugwiseConfigEntry,
458
  async_add_entities: AddConfigEntryEntitiesCallback,
459
  ) -> None:
460
+ """Set up Plugwise sensors from a config entry."""
461
+ # Upstream as Plugwise not Smile
462
  coordinator = entry.runtime_data
463
 
464
  @callback
 
467
  if not coordinator.new_devices:
468
  return
469
 
470
+ # Upstream consts
471
+ # async_add_entities(
472
+ # PlugwiseSensorEntity(coordinator, device_id, description)
473
+ # for device_id in coordinator.new_devices
474
+ # if (sensors := coordinator.data.devices[device_id].get(SENSORS))
475
+ # for description in PLUGWISE_SENSORS
476
+ # if description.key in sensors
477
+ # )
478
+ # pw-beta alternative for debugging
479
+ entities: list[PlugwiseSensorEntity] = []
480
+ for device_id in coordinator.new_devices:
481
+ device = coordinator.data[device_id]
482
+ if not (sensors := device.get(SENSORS)):
483
+ continue
484
+ for description in PLUGWISE_SENSORS:
485
+ if description.key not in sensors:
486
+ continue
487
+ entities.append(PlugwiseSensorEntity(coordinator, device_id, description))
488
+ LOGGER.debug(
489
+ "Add %s %s sensor", device["name"], description.translation_key or description.key
490
+ )
491
+
492
+ async_add_entities(entities)
493
 
494
  _add_entities()
495
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
508
  ) -> None:
509
  """Initialise the sensor."""
510
  super().__init__(coordinator, device_id)
 
511
  self.entity_description = description
512
+ self._attr_unique_id = f"{device_id}-{description.key}"
513
 
514
  @property
515
  @override
516
+ def native_value(self) -> int | float | None:
517
  """Return the value reported by the sensor."""
518
+ return self.device.get(SENSORS, {}).get(self.entity_description.key) # Upstream consts
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/switch.py RENAMED
@@ -14,6 +14,16 @@
14
  from homeassistant.core import HomeAssistant, callback
15
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
16
 
 
 
 
 
 
 
 
 
 
 
17
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
18
  from .entity import PlugwiseEntity
19
  from .util import plugwise_command
@@ -28,25 +38,23 @@
28
  key: SwitchType
29
 
30
 
31
- SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
32
- PlugwiseSwitchEntityDescription(
33
- key="dhw_cm_switch",
34
- translation_key="dhw_cm_switch",
35
- entity_category=EntityCategory.CONFIG,
36
- ),
37
  PlugwiseSwitchEntityDescription(
38
- key="lock",
39
- translation_key="lock",
 
40
  entity_category=EntityCategory.CONFIG,
41
  ),
42
  PlugwiseSwitchEntityDescription(
43
- key="relay",
44
- translation_key="relay",
45
  device_class=SwitchDeviceClass.SWITCH,
46
  ),
47
  PlugwiseSwitchEntityDescription(
48
- key="cooling_ena_switch",
49
- translation_key="cooling_ena_switch",
 
50
  entity_category=EntityCategory.CONFIG,
51
  ),
52
  )
@@ -57,7 +65,7 @@
57
  entry: PlugwiseConfigEntry,
58
  async_add_entities: AddConfigEntryEntitiesCallback,
59
  ) -> None:
60
- """Set up the Smile switches from a config entry."""
61
  coordinator = entry.runtime_data
62
 
63
  @callback
@@ -66,13 +74,28 @@
66
  if not coordinator.new_devices:
67
  return
68
 
69
- async_add_entities(
70
- PlugwiseSwitchEntity(coordinator, device_id, description)
71
- for device_id in coordinator.new_devices
72
- if (switches := coordinator.data[device_id].get("switches"))
73
- for description in SWITCHES
74
- if description.key in switches
75
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  _add_entities()
78
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -91,14 +114,14 @@
91
  ) -> None:
92
  """Set up the Plugwise API."""
93
  super().__init__(coordinator, device_id)
94
- self._attr_unique_id = f"{device_id}-{description.key}"
95
  self.entity_description = description
 
96
 
97
  @property
98
  @override
99
- def is_on(self) -> bool:
100
  """Return True if entity is on."""
101
- return self.device["switches"][self.entity_description.key]
102
 
103
  @plugwise_command
104
  @override
@@ -106,10 +129,10 @@
106
  """Turn the device on."""
107
  await self.coordinator.api.set_switch_state(
108
  self._dev_id,
109
- self.device.get("members"),
110
  self.entity_description.key,
111
  "on",
112
- )
113
 
114
  @plugwise_command
115
  @override
@@ -117,7 +140,7 @@
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
- )
 
14
  from homeassistant.core import HomeAssistant, callback
15
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
16
 
17
+ from .const import (
18
+ COOLING_ENA_SWITCH,
19
+ LOCK,
20
+ LOGGER, # pw-beta
21
+ MEMBERS,
22
+ RELAY,
23
+ SWITCHES,
24
+ )
25
+
26
+ # Upstream consts
27
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
28
  from .entity import PlugwiseEntity
29
  from .util import plugwise_command
 
38
  key: SwitchType
39
 
40
 
41
+ # Upstream consts
42
+ PLUGWISE_SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
 
 
 
 
43
  PlugwiseSwitchEntityDescription(
44
+ key=LOCK,
45
+ translation_key=LOCK,
46
+ device_class=SwitchDeviceClass.SWITCH,
47
  entity_category=EntityCategory.CONFIG,
48
  ),
49
  PlugwiseSwitchEntityDescription(
50
+ key=RELAY,
51
+ translation_key=RELAY,
52
  device_class=SwitchDeviceClass.SWITCH,
53
  ),
54
  PlugwiseSwitchEntityDescription(
55
+ key=COOLING_ENA_SWITCH,
56
+ translation_key=COOLING_ENA_SWITCH,
57
+ device_class=SwitchDeviceClass.SWITCH,
58
  entity_category=EntityCategory.CONFIG,
59
  ),
60
  )
 
65
  entry: PlugwiseConfigEntry,
66
  async_add_entities: AddConfigEntryEntitiesCallback,
67
  ) -> None:
68
+ """Set up Plugwise switches from a config entry."""
69
  coordinator = entry.runtime_data
70
 
71
  @callback
 
74
  if not coordinator.new_devices:
75
  return
76
 
77
+ # Upstream consts
78
+ # async_add_entities(
79
+ # PlugwiseSwitchEntity(coordinator, device_id, description)
80
+ # for device_id in coordinator.new_devices
81
+ # if (switches := coordinator.data.devices[device_id].get(SWITCHES))
82
+ # for description in PLUGWISE_SWITCHES
83
+ # if description.key in switches
84
+ # )
85
+ # pw-beta alternative for debugging
86
+ entities: list[PlugwiseSwitchEntity] = []
87
+ for device_id in coordinator.new_devices:
88
+ device = coordinator.data[device_id]
89
+ if not (switches := device.get(SWITCHES)):
90
+ continue
91
+ for description in PLUGWISE_SWITCHES:
92
+ if description.key not in switches:
93
+ continue
94
+ entities.append(PlugwiseSwitchEntity(coordinator, device_id, description))
95
+ LOGGER.debug(
96
+ "Add %s %s switch", device["name"], description.translation_key
97
+ )
98
+ async_add_entities(entities)
99
 
100
  _add_entities()
101
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
114
  ) -> None:
115
  """Set up the Plugwise API."""
116
  super().__init__(coordinator, device_id)
 
117
  self.entity_description = description
118
+ self._attr_unique_id = f"{device_id}-{description.key}"
119
 
120
  @property
121
  @override
122
+ def is_on(self) -> bool | None:
123
  """Return True if entity is on."""
124
+ return self.device.get(SWITCHES, {}).get(self.entity_description.key) # Upstream const
125
 
126
  @plugwise_command
127
  @override
 
129
  """Turn the device on."""
130
  await self.coordinator.api.set_switch_state(
131
  self._dev_id,
132
+ self.device.get(MEMBERS),
133
  self.entity_description.key,
134
  "on",
135
+ ) # Upstream const
136
 
137
  @plugwise_command
138
  @override
 
140
  """Turn the device off."""
141
  await self.coordinator.api.set_switch_state(
142
  self._dev_id,
143
+ self.device.get(MEMBERS),
144
  self.entity_description.key,
145
  "off",
146
+ ) # Upstream const
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/util.py RENAMED
@@ -10,10 +10,15 @@
10
  from .const import DOMAIN
11
  from .entity import PlugwiseEntity
12
 
 
 
 
 
13
 
14
- def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
15
- func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],
16
- ) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
 
17
  """Decorate Plugwise calls that send commands/make changes to the device.
18
 
19
  A decorator that wraps the passed in function, catches Plugwise errors,
@@ -21,8 +26,8 @@
21
  """
22
 
23
  async def handler(
24
- self: _PlugwiseEntityT, *args: _P.args, **kwargs: _P.kwargs
25
- ) -> _R:
26
  try:
27
  return await func(self, *args, **kwargs)
28
  except PlugwiseException as err:
 
10
  from .const import DOMAIN
11
  from .entity import PlugwiseEntity
12
 
13
+ # For reference:
14
+ # PlugwiseEntityT = TypeVar("PlugwiseEntityT", bound=PlugwiseEntity)
15
+ # R = TypeVar("_R")
16
+ # P = ParamSpec("_P")
17
 
18
+
19
+ def plugwise_command[PlugwiseEntityT: PlugwiseEntity, **P, R](
20
+ func: Callable[Concatenate[PlugwiseEntityT, P], Awaitable[R]],
21
+ ) -> Callable[Concatenate[PlugwiseEntityT, P], Coroutine[Any, Any, R]]:
22
  """Decorate Plugwise calls that send commands/make changes to the device.
23
 
24
  A decorator that wraps the passed in function, catches Plugwise errors,
 
26
  """
27
 
28
  async def handler(
29
+ self: PlugwiseEntityT, *args: P.args, **kwargs: P.kwargs
30
+ ) -> R:
31
  try:
32
  return await func(self, *args, **kwargs)
33
  except PlugwiseException as err: