Skip to content

Commit 07f4421

Browse files
authored
Merge pull request #314 from plugwise/mdi_sed
Scan updates
2 parents 23626fe + ca6f969 commit 07f4421

File tree

17 files changed

+268
-236
lines changed

17 files changed

+268
-236
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
## Versions from 0.4x
44

5-
### Ongoing
5+
### v0.55.9 - 2025-08-25
66

7+
- Add select for Scan sensitivity
8+
- Add button for Scan light calibration
79
- Fix disabled buttons for all non-plus devices
810
- Shorten/correct logger-messages to use `node_duc.node.name`
911

custom_components/plugwise_usb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async def async_unload_entry(
164164
"""Unload the Plugwise USB stick connection."""
165165
config_entry.runtime_data[UNSUBSCRIBE_DISCOVERY]()
166166
for coordinator in config_entry.runtime_data[NODES].values():
167-
coordinator.unsubscribe_all_nodefeatures()
167+
await coordinator.unsubscribe_all_nodefeatures()
168168
unload = await hass.config_entries.async_unload_platforms(
169169
config_entry, PLUGWISE_USB_PLATFORMS
170170
)

custom_components/plugwise_usb/binary_sensor.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
BinarySensorEntity,
1414
BinarySensorEntityDescription,
1515
)
16-
from homeassistant.const import Platform
16+
from homeassistant.const import EntityCategory, Platform
1717
from homeassistant.core import HomeAssistant, callback
1818
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1919

@@ -43,6 +43,24 @@ class PlugwiseBinarySensorEntityDescription(
4343
node_feature=NodeFeature.MOTION,
4444
api_attribute="state",
4545
),
46+
PlugwiseBinarySensorEntityDescription(
47+
key="motion_config_dirty",
48+
translation_key="motion_config_dirty",
49+
node_feature=NodeFeature.MOTION_CONFIG,
50+
device_class=BinarySensorDeviceClass.SAFETY,
51+
entity_category=EntityCategory.DIAGNOSTIC,
52+
entity_registry_enabled_default=False,
53+
api_attribute="dirty",
54+
),
55+
PlugwiseBinarySensorEntityDescription(
56+
key="battery_config_dirty",
57+
translation_key="battery_config_dirty",
58+
node_feature=NodeFeature.BATTERY,
59+
device_class=BinarySensorDeviceClass.SAFETY,
60+
entity_category=EntityCategory.DIAGNOSTIC,
61+
entity_registry_enabled_default=False,
62+
api_attribute="dirty",
63+
),
4664
)
4765

4866

@@ -70,7 +88,7 @@ async def async_add_binary_sensor(node_event: NodeEvent, mac: str) -> None:
7088
]
7189
)
7290
if entities:
73-
async_add_entities(entities, update_before_add=True)
91+
async_add_entities(entities)
7492

7593
api_stick = config_entry.runtime_data[STICK]
7694

custom_components/plugwise_usb/button.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ class PlugwiseButtonEntityDescription(
4646
async_button_fn="energy_reset_request",
4747
node_feature=NodeFeature.CIRCLE,
4848
),
49+
PlugwiseButtonEntityDescription(
50+
key="ping_node",
51+
translation_key="ping_node",
52+
entity_category=EntityCategory.CONFIG,
53+
async_button_fn="ping_update",
54+
node_feature=NodeFeature.CIRCLE,
55+
),
56+
PlugwiseButtonEntityDescription(
57+
key="calibrate_light",
58+
translation_key="calibrate_light",
59+
entity_category=EntityCategory.CONFIG,
60+
async_button_fn="scan_calibrate_light",
61+
node_feature=NodeFeature.MOTION,
62+
),
4963
)
5064

5165

custom_components/plugwise_usb/const.py

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
PLUGWISE_USB_PLATFORMS: Final[list[str]] = [
2626
Platform.BINARY_SENSOR,
2727
Platform.NUMBER,
28+
Platform.SELECT,
2829
Platform.SENSOR,
2930
Platform.SWITCH,
3031
Platform.BUTTON,
@@ -50,24 +51,6 @@
5051
ATTR_SED_CLOCK_SYNC: Final[str] = "clock_sync"
5152
ATTR_SED_CLOCK_INTERVAL: Final[str] = "clock_interval"
5253

53-
SERVICE_USB_SED_BATTERY_CONFIG: Final[str] = "configure_battery_savings"
54-
SERVICE_USB_SED_BATTERY_CONFIG_SCHEMA: Final = {
55-
vol.Required(ATTR_SED_STAY_ACTIVE): vol.All(
56-
vol.Coerce(int), vol.Range(min=1, max=120)
57-
),
58-
vol.Required(ATTR_SED_SLEEP_FOR): vol.All(
59-
vol.Coerce(int), vol.Range(min=10, max=60)
60-
),
61-
vol.Required(ATTR_SED_MAINTENANCE_INTERVAL): vol.All(
62-
vol.Coerce(int), vol.Range(min=5, max=1440)
63-
),
64-
vol.Required(ATTR_SED_CLOCK_SYNC): cv.boolean,
65-
vol.Required(ATTR_SED_CLOCK_INTERVAL): vol.All(
66-
vol.Coerce(int), vol.Range(min=60, max=10080)
67-
),
68-
}
69-
70-
7154
# USB Scan device constants
7255
ATTR_SCAN_DAYLIGHT_MODE: Final[str] = "day_light"
7356
ATTR_SCAN_SENSITIVITY_MODE: Final[str] = "sensitivity_mode"
@@ -81,14 +64,3 @@
8164
SCAN_SENSITIVITY_MEDIUM,
8265
SCAN_SENSITIVITY_OFF,
8366
]
84-
85-
SERVICE_USB_SCAN_CONFIG: Final[str] = "configure_scan"
86-
SERVICE_USB_SCAN_CONFIG_SCHEMA = (
87-
{
88-
vol.Required(ATTR_SCAN_SENSITIVITY_MODE): vol.In(SCAN_SENSITIVITY_MODES),
89-
vol.Required(ATTR_SCAN_RESET_TIMER): vol.All(
90-
vol.Coerce(int), vol.Range(min=1, max=240)
91-
),
92-
vol.Required(ATTR_SCAN_DAYLIGHT_MODE): cv.boolean,
93-
},
94-
)

custom_components/plugwise_usb/coordinator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ async def async_node_update(self) -> dict[NodeFeature, Any]:
8787
and self.node.initialized
8888
and not states[NodeFeature.AVAILABLE].state
8989
):
90-
raise UpdateFailed("Device is not responding")
90+
raise UpdateFailed(
91+
f"Device '{self.node.node_info.mac}' is (temporarily) not available"
92+
)
9193

9294
return states
9395

custom_components/plugwise_usb/event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ async def async_add_event(node_event: NodeEvent, mac: str) -> None:
9292
]
9393
)
9494
if entities:
95-
async_add_entities(entities, update_before_add=True)
95+
async_add_entities(entities)
9696

9797
api_stick = config_entry.runtime_data[STICK]
9898

custom_components/plugwise_usb/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
"integration_type": "hub",
99
"iot_class": "local_polling",
1010
"loggers": ["plugwise_usb"],
11-
"requirements": ["plugwise-usb==0.44.11"],
12-
"version": "0.55.8"
11+
"requirements": ["plugwise-usb==0.44.12"],
12+
"version": "0.55.9"
1313
}

custom_components/plugwise_usb/number.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class PlugwiseNumberEntityDescription(
6969
device_class=NumberDeviceClass.DURATION,
7070
native_unit_of_measurement=UnitOfTime.MINUTES,
7171
entity_category=EntityCategory.CONFIG,
72-
native_max_value=65535,
72+
native_max_value=1440,
7373
native_min_value=60,
7474
api_attribute="sleep_duration",
7575
),
@@ -81,7 +81,7 @@ class PlugwiseNumberEntityDescription(
8181
device_class=NumberDeviceClass.DURATION,
8282
native_unit_of_measurement=UnitOfTime.SECONDS,
8383
entity_category=EntityCategory.CONFIG,
84-
native_max_value=255,
84+
native_max_value=60,
8585
native_min_value=1,
8686
api_attribute="awake_duration",
8787
),
@@ -122,7 +122,7 @@ async def async_add_number(node_event: NodeEvent, mac: str) -> None:
122122
]
123123
)
124124
if entities:
125-
async_add_entities(entities, update_before_add=True)
125+
async_add_entities(entities)
126126

127127
api_stick = config_entry.runtime_data[STICK]
128128

@@ -182,5 +182,5 @@ def _handle_coordinator_update(self) -> None:
182182
async def async_set_native_value(self, value: float) -> None:
183183
"""Update the current value."""
184184

185-
self._attr_native_value = await self.async_number_fn(int(value))
185+
await self.async_number_fn(int(value))
186186
self.async_write_ha_state()
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Plugwise USB Select component for HomeAssistant."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from datetime import timedelta
7+
from enum import Enum
8+
import logging
9+
10+
from plugwise_usb.api import MotionSensitivity, NodeEvent, NodeFeature
11+
12+
from homeassistant.components.select import SelectEntity, SelectEntityDescription
13+
from homeassistant.const import EntityCategory, Platform
14+
from homeassistant.core import HomeAssistant, callback
15+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
16+
17+
from .const import NODES, STICK, UNSUB_NODE_LOADED
18+
from .coordinator import PlugwiseUSBConfigEntry, PlugwiseUSBDataUpdateCoordinator
19+
from .entity import PlugwiseUSBEntity, PlugwiseUSBEntityDescription
20+
21+
_LOGGER = logging.getLogger(__name__)
22+
PARALLEL_UPDATES = 2
23+
SCAN_INTERVAL = timedelta(seconds=30)
24+
25+
26+
@dataclass(kw_only=True)
27+
class PlugwiseSelectEntityDescription(
28+
PlugwiseUSBEntityDescription, SelectEntityDescription
29+
):
30+
"""Describes Plugwise select entity."""
31+
32+
async_select_fn: str = ""
33+
options_enum: type[Enum]
34+
35+
SELECT_TYPES: tuple[PlugwiseSelectEntityDescription, ...] = (
36+
PlugwiseSelectEntityDescription(
37+
key="sensitivity_level",
38+
translation_key="motion_sensitivity_level",
39+
async_select_fn="set_motion_sensitivity_level",
40+
entity_category=EntityCategory.CONFIG,
41+
node_feature=NodeFeature.MOTION_CONFIG,
42+
options_enum = MotionSensitivity,
43+
),
44+
)
45+
46+
47+
async def async_setup_entry(
48+
_hass: HomeAssistant,
49+
config_entry: PlugwiseUSBConfigEntry,
50+
async_add_entities: AddEntitiesCallback,
51+
) -> None:
52+
"""Set up the USB selects from a config entry."""
53+
54+
async def async_add_select(node_event: NodeEvent, mac: str) -> None:
55+
"""Initialize DUC for select."""
56+
if node_event != NodeEvent.LOADED:
57+
return
58+
entities: list[PlugwiseUSBEntity] = []
59+
if (node_duc := config_entry.runtime_data[NODES].get(mac)) is not None:
60+
_LOGGER.debug("Add select entities for node %s", node_duc.node.name)
61+
entities.extend(
62+
[
63+
PlugwiseUSBSelectEntity(node_duc, entity_description)
64+
for entity_description in SELECT_TYPES
65+
if entity_description.node_feature in node_duc.node.features
66+
]
67+
)
68+
if entities:
69+
async_add_entities(entities)
70+
71+
api_stick = config_entry.runtime_data[STICK]
72+
73+
# Listen for loaded nodes
74+
config_entry.runtime_data[Platform.SELECT] = {}
75+
config_entry.runtime_data[Platform.SELECT][UNSUB_NODE_LOADED] = (
76+
api_stick.subscribe_to_node_events(
77+
async_add_select,
78+
(NodeEvent.LOADED,),
79+
)
80+
)
81+
82+
# load any current nodes
83+
for mac, node in api_stick.nodes.items():
84+
if node.is_loaded:
85+
await async_add_select(NodeEvent.LOADED, mac)
86+
87+
88+
async def async_unload_entry(
89+
_hass: HomeAssistant,
90+
config_entry: PlugwiseUSBConfigEntry,
91+
) -> None:
92+
"""Unload a config entry."""
93+
config_entry.runtime_data[Platform.SELECT][UNSUB_NODE_LOADED]()
94+
95+
96+
class PlugwiseUSBSelectEntity(PlugwiseUSBEntity, SelectEntity):
97+
"""Representation of a Plugwise USB Data Update Coordinator select."""
98+
99+
def __init__(
100+
self,
101+
node_duc: PlugwiseUSBDataUpdateCoordinator,
102+
entity_description: PlugwiseSelectEntityDescription,
103+
) -> None:
104+
"""Initialize a select entity."""
105+
super().__init__(node_duc, entity_description)
106+
self.async_select_fn = getattr(
107+
node_duc.node, entity_description.async_select_fn
108+
)
109+
self._attr_options = [o.name.lower() for o in entity_description.options_enum]
110+
111+
@callback
112+
def _handle_coordinator_update(self) -> None:
113+
"""Handle updated data from the coordinator."""
114+
data = self.coordinator.data.get(self.entity_description.node_feature, None)
115+
if data is None:
116+
_LOGGER.debug(
117+
"No %s select data for %s",
118+
str(self.entity_description.node_feature),
119+
self._node_info.mac,
120+
)
121+
return
122+
123+
current_option = getattr(
124+
data,
125+
self.entity_description.key,
126+
)
127+
self._attr_current_option = current_option.name.lower()
128+
self.async_write_ha_state()
129+
130+
async def async_select_option(self, option: str) -> None:
131+
"""Change to the selected entity option."""
132+
normalized = option.strip().lower()
133+
if normalized not in self._attr_options:
134+
raise ValueError(f"Unsupported option: {option}")
135+
value = self.entity_description.options_enum[normalized.upper()]
136+
await self.async_select_fn(value)
137+
self._attr_current_option = normalized
138+
self.async_write_ha_state()

0 commit comments

Comments
 (0)