Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ceb445a
add binary sensor to show the configuration has changed but the node …
dirixmjm Aug 11, 2025
2d1333c
missing await
dirixmjm Aug 13, 2025
df195ad
binary_sensor cannot be inside CONFIG catagory
dirixmjm Aug 13, 2025
5ff335e
set more sensible value ranges, remove assigned of parameter as the n…
dirixmjm Aug 13, 2025
066e725
improve error notice on device unavailable
dirixmjm Aug 18, 2025
618738c
no need to poll on start-up, prevents not-available messages
dirixmjm Aug 18, 2025
b080310
implement select function for MotionSensitivity
dirixmjm Aug 18, 2025
206fcaa
make notification more verbose
dirixmjm Aug 18, 2025
9a53c19
fix translations
dirixmjm Aug 18, 2025
e6ecf92
CF: remove space
dirixmjm Aug 18, 2025
ab3a215
CR: Recommendations on updates and imprt Enum
dirixmjm Aug 18, 2025
6a06026
CR: Fix typo
dirixmjm Aug 18, 2025
99b9b31
Add ping button
dirixmjm Aug 20, 2025
c2e189a
change translation keys according to CR whishes
dirixmjm Aug 20, 2025
260fc50
button cannot be inside diagnostics for some reason
dirixmjm Aug 20, 2025
5655877
fix lowercase keys in translations, which is required according to ha…
dirixmjm Aug 24, 2025
8d1ac07
CR: improve exception
dirixmjm Aug 24, 2025
2683340
Link to plugwise_usb v0.44.12
bouwew Aug 24, 2025
9264fa3
Ruff updates
bouwew Aug 24, 2025
4e09652
Coordinator: add space in logging-message
bouwew Aug 24, 2025
f0d780b
Sorting, cleanup
bouwew Aug 24, 2025
f9ed3b3
Use _attr_options, then no property required
bouwew Aug 24, 2025
12dd8d5
Use _attr_current_option as well
bouwew Aug 24, 2025
9520f96
Remove api_attribute, same as key
bouwew Aug 24, 2025
6bf585e
Bump version to v0.55.9(a0)
bouwew Aug 24, 2025
4d835f3
CR: fix node_duc reference
dirixmjm Aug 25, 2025
49bbea1
CR: nitpick typo
dirixmjm Aug 25, 2025
3db9f5a
CR: move options to options_enum
dirixmjm Aug 25, 2025
477b64e
fix type
dirixmjm Aug 25, 2025
0195b7e
bouwew: fixes to select.py
dirixmjm Aug 25, 2025
9802ab2
version bump
dirixmjm Aug 25, 2025
ca6f969
change to lowercase
dirixmjm Aug 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom_components/plugwise_usb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ async def async_unload_entry(
"""Unload the Plugwise USB stick connection."""
config_entry.runtime_data[UNSUBSCRIBE_DISCOVERY]()
for coordinator in config_entry.runtime_data[NODES].values():
coordinator.unsubscribe_all_nodefeatures()
await coordinator.unsubscribe_all_nodefeatures()
unload = await hass.config_entries.async_unload_platforms(
config_entry, PLUGWISE_USB_PLATFORMS
)
Expand Down
22 changes: 20 additions & 2 deletions custom_components/plugwise_usb/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import Platform
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback

Expand Down Expand Up @@ -43,6 +43,24 @@ class PlugwiseBinarySensorEntityDescription(
node_feature=NodeFeature.MOTION,
api_attribute="state",
),
PlugwiseBinarySensorEntityDescription(
key="motion_config_dirty",
translation_key="motion_config_dirty",
node_feature=NodeFeature.MOTION_CONFIG,
device_class=BinarySensorDeviceClass.SAFETY,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
api_attribute="dirty",
),
PlugwiseBinarySensorEntityDescription(
key="battery_config_dirty",
translation_key="battery_config_dirty",
node_feature=NodeFeature.BATTERY,
device_class=BinarySensorDeviceClass.SAFETY,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
api_attribute="dirty",
),
)


Expand Down Expand Up @@ -70,7 +88,7 @@ async def async_add_binary_sensor(node_event: NodeEvent, mac: str) -> None:
]
)
if entities:
async_add_entities(entities, update_before_add=True)
async_add_entities(entities)

api_stick = config_entry.runtime_data[STICK]

Expand Down
14 changes: 14 additions & 0 deletions custom_components/plugwise_usb/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ class PlugwiseButtonEntityDescription(
async_button_fn="energy_reset_request",
node_feature=NodeFeature.CIRCLE,
),
PlugwiseButtonEntityDescription(
key="ping_node",
translation_key="ping_node",
entity_category=EntityCategory.CONFIG,
async_button_fn="ping_update",
node_feature=NodeFeature.CIRCLE,
),
PlugwiseButtonEntityDescription(
key="calibrate_light",
translation_key="calibrate_light",
entity_category=EntityCategory.CONFIG,
async_button_fn="scan_calibrate_light",
node_feature=NodeFeature.MOTION,
),
)


Expand Down
30 changes: 1 addition & 29 deletions custom_components/plugwise_usb/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
PLUGWISE_USB_PLATFORMS: Final[list[str]] = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Expand All @@ -50,24 +51,6 @@
ATTR_SED_CLOCK_SYNC: Final[str] = "clock_sync"
ATTR_SED_CLOCK_INTERVAL: Final[str] = "clock_interval"

SERVICE_USB_SED_BATTERY_CONFIG: Final[str] = "configure_battery_savings"
SERVICE_USB_SED_BATTERY_CONFIG_SCHEMA: Final = {
vol.Required(ATTR_SED_STAY_ACTIVE): vol.All(
vol.Coerce(int), vol.Range(min=1, max=120)
),
vol.Required(ATTR_SED_SLEEP_FOR): vol.All(
vol.Coerce(int), vol.Range(min=10, max=60)
),
vol.Required(ATTR_SED_MAINTENANCE_INTERVAL): vol.All(
vol.Coerce(int), vol.Range(min=5, max=1440)
),
vol.Required(ATTR_SED_CLOCK_SYNC): cv.boolean,
vol.Required(ATTR_SED_CLOCK_INTERVAL): vol.All(
vol.Coerce(int), vol.Range(min=60, max=10080)
),
}


# USB Scan device constants
ATTR_SCAN_DAYLIGHT_MODE: Final[str] = "day_light"
ATTR_SCAN_SENSITIVITY_MODE: Final[str] = "sensitivity_mode"
Expand All @@ -81,14 +64,3 @@
SCAN_SENSITIVITY_MEDIUM,
SCAN_SENSITIVITY_OFF,
]

SERVICE_USB_SCAN_CONFIG: Final[str] = "configure_scan"
SERVICE_USB_SCAN_CONFIG_SCHEMA = (
{
vol.Required(ATTR_SCAN_SENSITIVITY_MODE): vol.In(SCAN_SENSITIVITY_MODES),
vol.Required(ATTR_SCAN_RESET_TIMER): vol.All(
vol.Coerce(int), vol.Range(min=1, max=240)
),
vol.Required(ATTR_SCAN_DAYLIGHT_MODE): cv.boolean,
},
)
4 changes: 3 additions & 1 deletion custom_components/plugwise_usb/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ async def async_node_update(self) -> dict[NodeFeature, Any]:
and self.node.initialized
and not states[NodeFeature.AVAILABLE].state
):
raise UpdateFailed("Device is not responding")
raise UpdateFailed(
f"Device '{self.node.node_info.mac}' is (temporarily) not available"
)

return states

Expand Down
2 changes: 1 addition & 1 deletion custom_components/plugwise_usb/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async def async_add_event(node_event: NodeEvent, mac: str) -> None:
]
)
if entities:
async_add_entities(entities, update_before_add=True)
async_add_entities(entities)

api_stick = config_entry.runtime_data[STICK]

Expand Down
4 changes: 2 additions & 2 deletions custom_components/plugwise_usb/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["plugwise_usb"],
"requirements": ["plugwise-usb==0.44.11"],
"version": "0.55.8"
"requirements": ["plugwise-usb==0.44.12"],
"version": "0.55.9"
}
8 changes: 4 additions & 4 deletions custom_components/plugwise_usb/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class PlugwiseNumberEntityDescription(
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
entity_category=EntityCategory.CONFIG,
native_max_value=65535,
native_max_value=1440,
native_min_value=60,
api_attribute="sleep_duration",
),
Expand All @@ -81,7 +81,7 @@ class PlugwiseNumberEntityDescription(
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
entity_category=EntityCategory.CONFIG,
native_max_value=255,
native_max_value=60,
native_min_value=1,
api_attribute="awake_duration",
),
Expand Down Expand Up @@ -122,7 +122,7 @@ async def async_add_number(node_event: NodeEvent, mac: str) -> None:
]
)
if entities:
async_add_entities(entities, update_before_add=True)
async_add_entities(entities)

api_stick = config_entry.runtime_data[STICK]

Expand Down Expand Up @@ -182,5 +182,5 @@ def _handle_coordinator_update(self) -> None:
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""

self._attr_native_value = await self.async_number_fn(int(value))
await self.async_number_fn(int(value))
self.async_write_ha_state()
139 changes: 139 additions & 0 deletions custom_components/plugwise_usb/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Plugwise USB Select component for HomeAssistant."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import timedelta
import logging

from plugwise_usb.api import MotionSensitivity, NodeEvent, NodeFeature

from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import NODES, STICK, UNSUB_NODE_LOADED
from .coordinator import PlugwiseUSBConfigEntry, PlugwiseUSBDataUpdateCoordinator
from .entity import PlugwiseUSBEntity, PlugwiseUSBEntityDescription

_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 2
SCAN_INTERVAL = timedelta(seconds=30)


@dataclass(kw_only=True)
class PlugwiseSelectEntityDescription(
PlugwiseUSBEntityDescription, SelectEntityDescription
):
"""Describes Plugwise select entity."""

async_select_fn: str = ""


SELECT_TYPES: tuple[PlugwiseSelectEntityDescription, ...] = (
PlugwiseSelectEntityDescription(
key="sensitivity_level",
translation_key="motion_sensitivity_level",
async_select_fn="set_motion_sensitivity_level",
entity_category=EntityCategory.CONFIG,
node_feature=NodeFeature.MOTION_CONFIG,
options = MotionSensitivity,
),
)


async def async_setup_entry(
_hass: HomeAssistant,
config_entry: PlugwiseUSBConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the USB selectes from a config entry."""

async def async_add_select(node_event: NodeEvent, mac: str) -> None:
"""Initialize DUC for select."""
if node_event != NodeEvent.LOADED:
return
entities: list[PlugwiseUSBEntity] = []
if (node_duc := config_entry.runtime_data[NODES].get(mac)) is not None:
_LOGGER.debug(
"Add select entities for %s | duc=%s", mac, node_duc.node.name
)
entities.extend(
[
PlugwiseUSBSelectEntity(node_duc, entity_description)
for entity_description in SELECT_TYPES
if entity_description.node_feature in node_duc.node.features
]
)
if entities:
async_add_entities(entities)

api_stick = config_entry.runtime_data[STICK]

# Listen for loaded nodes
config_entry.runtime_data[Platform.SELECT] = {}
config_entry.runtime_data[Platform.SELECT][UNSUB_NODE_LOADED] = (
api_stick.subscribe_to_node_events(
async_add_select,
(NodeEvent.LOADED,),
)
)

# load any current nodes
for mac, node in api_stick.nodes.items():
if node.is_loaded:
await async_add_select(NodeEvent.LOADED, mac)


async def async_unload_entry(
_hass: HomeAssistant,
config_entry: PlugwiseUSBConfigEntry,
) -> None:
"""Unload a config entry."""
config_entry.runtime_data[Platform.SELECT][UNSUB_NODE_LOADED]()


class PlugwiseUSBSelectEntity(PlugwiseUSBEntity, SelectEntity):
"""Representation of a Plugwise USB Data Update Coordinator select."""

def __init__(
self,
node_duc: PlugwiseUSBDataUpdateCoordinator,
entity_description: PlugwiseSelectEntityDescription,
) -> None:
"""Initialize a select entity."""
super().__init__(node_duc, entity_description)
self.async_select_fn = getattr(
node_duc.node, entity_description.async_select_fn
)
self._attr_options: list[str] = [o.name.lower() for o in entity_description.options]
self.entity_description = entity_description
self._node_duc = node_duc

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
data = self.coordinator.data.get(self.entity_description.node_feature, None)
if data is None:
_LOGGER.debug(
"No %s select data for %s",
str(self.entity_description.node_feature),
self._node_info.mac,
)
return

current_option = getattr(
data,
self.entity_description.key,
)
self._attr_current_option = current_option.name.lower()
self.async_write_ha_state()

async def async_select_option(self, option: str) -> None:
"""Change to the selected entity option."""
value = self.entity_description.options[option.upper()]
await self.async_select_fn(value)
self._current_option = option
self._attr_current_option = option
self.async_write_ha_state()
2 changes: 1 addition & 1 deletion custom_components/plugwise_usb/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async def async_add_sensor(node_event: NodeEvent, mac: str) -> None:
_LOGGER.debug("async_add_sensor | %s | GET MAC FAILED", mac)

if entities:
async_add_entities(entities, update_before_add=True)
async_add_entities(entities)

api_stick = config_entry.runtime_data[STICK]

Expand Down
24 changes: 0 additions & 24 deletions custom_components/plugwise_usb/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,3 @@ disable_production:
fields:
mac:
example: "data: {mac: 0123456789ABCDEF}"
configure_scan:
fields:
entity_id:
example: binary_sensor.motion_AB123
sensitivity_mode:
example: medium
reset_timer:
example: 5
day_light:
example: False
configure_battery_savings:
fields:
entity_id:
example: binary_sensor.motion_AB123
stay_active:
example: 10
sleep_for:
example: 60
maintenance_interval:
example: 1440
clock_sync:
example: False
clock_interval:
example: 10080
Loading
Loading