Add binary_sensor to homeworks (#112418)

* Add binary_sensor to homeworks

* Update tests
This commit is contained in:
Erik Montnemery 2024-03-12 19:25:27 +01:00 committed by GitHub
parent b670066c00
commit 2cdf6b9937
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 183 additions and 31 deletions

View File

@ -546,6 +546,7 @@ omit =
homeassistant/components/homematic/sensor.py
homeassistant/components/homematic/switch.py
homeassistant/components/homeworks/__init__.py
homeassistant/components/homeworks/binary_sensor.py
homeassistant/components/homeworks/button.py
homeassistant/components/homeworks/light.py
homeassistant/components/horizon/media_player.py

View File

@ -21,6 +21,7 @@ from homeassistant.const import (
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType
@ -37,13 +38,14 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.LIGHT]
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
EVENT_BUTTON_PRESS = "homeworks_button_press"
EVENT_BUTTON_RELEASE = "homeworks_button_release"
DEFAULT_FADE_RATE = 1.0
KEYPAD_LEDSTATE_POLL_COOLDOWN = 1.0
CV_FADE_RATE = vol.All(vol.Coerce(float), vol.Range(min=0, max=20))
@ -208,6 +210,13 @@ class HomeworksKeypad:
"""Register callback that will be used for signals."""
self._addr = addr
self._controller = controller
self._debouncer = Debouncer(
hass,
_LOGGER,
cooldown=KEYPAD_LEDSTATE_POLL_COOLDOWN,
immediate=False,
function=self._request_keypad_led_states,
)
self._hass = hass
self._name = name
self._id = slugify(self._name)
@ -229,3 +238,15 @@ class HomeworksKeypad:
return
data = {CONF_ID: self._id, CONF_NAME: self._name, "button": values[1]}
self._hass.bus.async_fire(event, data)
def _request_keypad_led_states(self) -> None:
"""Query keypad led state."""
# pylint: disable-next=protected-access
self._controller._send(f"RKLS, {self._addr}")
async def request_keypad_led_states(self) -> None:
"""Query keypad led state.
Debounced to not storm the controller during setup.
"""
await self._debouncer.async_call()

View File

@ -0,0 +1,93 @@
"""Support for Lutron Homeworks binary sensors."""
from __future__ import annotations
import logging
from typing import Any
from pyhomeworks.pyhomeworks import HW_KEYPAD_LED_CHANGED, Homeworks
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeworksData, HomeworksEntity, HomeworksKeypad
from .const import (
CONF_ADDR,
CONF_BUTTONS,
CONF_CONTROLLER_ID,
CONF_KEYPADS,
CONF_LED,
CONF_NUMBER,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Homeworks binary sensors."""
data: HomeworksData = hass.data[DOMAIN][entry.entry_id]
controller = data.controller
controller_id = entry.options[CONF_CONTROLLER_ID]
devs = []
for keypad in entry.options.get(CONF_KEYPADS, []):
for button in keypad[CONF_BUTTONS]:
if not button[CONF_LED]:
continue
dev = HomeworksBinarySensor(
controller,
data.keypads[keypad[CONF_ADDR]],
controller_id,
keypad[CONF_ADDR],
keypad[CONF_NAME],
button[CONF_NAME],
button[CONF_NUMBER],
)
devs.append(dev)
async_add_entities(devs, True)
class HomeworksBinarySensor(HomeworksEntity, BinarySensorEntity):
"""Homeworks Binary Sensor."""
_attr_has_entity_name = True
def __init__(
self,
controller: Homeworks,
keypad: HomeworksKeypad,
controller_id: str,
addr: str,
keypad_name: str,
button_name: str,
led_number: int,
) -> None:
"""Create device with Addr, name, and rate."""
super().__init__(controller, controller_id, addr, led_number, button_name)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{controller_id}.{addr}")}, name=keypad_name
)
self._keypad = keypad
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
signal = f"homeworks_entity_{self._controller_id}_{self._addr}"
_LOGGER.debug("connecting %s", signal)
self.async_on_remove(
async_dispatcher_connect(self.hass, signal, self._update_callback)
)
await self._keypad.request_keypad_led_states()
@callback
def _update_callback(self, msg_type: str, values: list[Any]) -> None:
"""Process device specific messages."""
if msg_type != HW_KEYPAD_LED_CHANGED or len(values[1]) < self._idx:
return
self._attr_is_on = bool(values[1][self._idx - 1])
self.async_write_ha_state()

View File

@ -9,6 +9,7 @@ from typing import Any
from pyhomeworks.pyhomeworks import Homeworks
import voluptuous as vol
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
@ -44,6 +45,7 @@ from .const import (
CONF_DIMMERS,
CONF_INDEX,
CONF_KEYPADS,
CONF_LED,
CONF_NUMBER,
CONF_RATE,
CONF_RELEASE_DELAY,
@ -78,6 +80,7 @@ LIGHT_EDIT = {
}
BUTTON_EDIT = {
vol.Optional(CONF_LED, default=False): selector.BooleanSelector(),
vol.Optional(CONF_RELEASE_DELAY, default=0): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
@ -380,16 +383,17 @@ async def validate_remove_button(
if str(index) not in removed_indexes:
items.append(item)
button_number = keypad[CONF_BUTTONS][index][CONF_NUMBER]
if entity_id := entity_registry.async_get_entity_id(
BUTTON_DOMAIN,
DOMAIN,
calculate_unique_id(
handler.options[CONF_CONTROLLER_ID],
keypad[CONF_ADDR],
button_number,
),
):
entity_registry.async_remove(entity_id)
for domain in (BINARY_SENSOR_DOMAIN, BUTTON_DOMAIN):
if entity_id := entity_registry.async_get_entity_id(
domain,
DOMAIN,
calculate_unique_id(
handler.options[CONF_CONTROLLER_ID],
keypad[CONF_ADDR],
button_number,
),
):
entity_registry.async_remove(entity_id)
keypad[CONF_BUTTONS] = items
return {}
@ -563,6 +567,7 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_ADDR: keypad[CONF_ADDR],
CONF_BUTTONS: [
{
CONF_LED: button[CONF_LED],
CONF_NAME: button[CONF_NAME],
CONF_NUMBER: button[CONF_NUMBER],
CONF_RELEASE_DELAY: button[CONF_RELEASE_DELAY],

View File

@ -10,6 +10,7 @@ CONF_CONTROLLER_ID = "controller_id"
CONF_DIMMERS = "dimmers"
CONF_INDEX = "index"
CONF_KEYPADS = "keypads"
CONF_LED = "led"
CONF_NUMBER = "number"
CONF_RATE = "rate"
CONF_RELEASE_DELAY = "release_delay"

View File

@ -60,10 +60,12 @@
"data": {
"name": "[%key:common::config_flow::data::name%]",
"number": "Number",
"led": "LED",
"release_delay": "Release delay"
},
"data_description": {
"number": "Button number in the range 1 to 24",
"led": "Enable if the button has a scene select indicator",
"release_delay": "Time between press and release, set to 0 to only press"
},
"title": "[%key:component::homeworks::options::step::init::menu_options::add_keypad%]"
@ -92,9 +94,11 @@
},
"edit_button": {
"data": {
"led": "[%key:component::homeworks::options::step::add_button::data::led%]",
"release_delay": "[%key:component::homeworks::options::step::add_button::data::release_delay%]"
},
"data_description": {
"led": "[%key:component::homeworks::options::step::add_button::data_description::led%]",
"release_delay": "[%key:component::homeworks::options::step::add_button::data_description::release_delay%]"
},
"title": "[%key:component::homeworks::options::step::edit_keypad::menu_options::select_edit_button%]"

View File

@ -11,6 +11,7 @@ from homeassistant.components.homeworks.const import (
CONF_CONTROLLER_ID,
CONF_DIMMERS,
CONF_KEYPADS,
CONF_LED,
CONF_NUMBER,
CONF_RATE,
CONF_RELEASE_DELAY,
@ -47,16 +48,19 @@ def mock_config_entry() -> MockConfigEntry:
{
CONF_NAME: "Morning",
CONF_NUMBER: 1,
CONF_LED: True,
CONF_RELEASE_DELAY: None,
},
{
CONF_NAME: "Relax",
CONF_NUMBER: 2,
CONF_LED: True,
CONF_RELEASE_DELAY: None,
},
{
CONF_NAME: "Dim up",
CONF_NUMBER: 3,
CONF_LED: False,
CONF_RELEASE_DELAY: 0.2,
},
],

View File

@ -5,6 +5,7 @@ from unittest.mock import ANY, MagicMock
import pytest
from pytest_unordered import unordered
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
from homeassistant.components.homeworks.const import (
CONF_ADDR,
@ -12,6 +13,7 @@ from homeassistant.components.homeworks.const import (
CONF_DIMMERS,
CONF_INDEX,
CONF_KEYPADS,
CONF_LED,
CONF_NUMBER,
CONF_RATE,
CONF_RELEASE_DELAY,
@ -163,16 +165,19 @@ async def test_import_flow(
{
CONF_NAME: "Morning",
CONF_NUMBER: 1,
CONF_LED: True,
CONF_RELEASE_DELAY: None,
},
{
CONF_NAME: "Relax",
CONF_NUMBER: 2,
CONF_LED: True,
CONF_RELEASE_DELAY: None,
},
{
CONF_NAME: "Dim up",
CONF_NUMBER: 3,
CONF_LED: False,
CONF_RELEASE_DELAY: 0.2,
},
],
@ -204,12 +209,13 @@ async def test_import_flow(
"addr": "[02:08:02:01]",
"buttons": [
{
"led": True,
"name": "Morning",
"number": 1,
"release_delay": None,
},
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"name": "Foyer Keypad",
}
@ -295,12 +301,13 @@ async def test_reconfigure_flow(
"addr": "[02:08:02:01]",
"buttons": [
{
"led": True,
"name": "Morning",
"number": 1,
"release_delay": None,
},
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"name": "Foyer Keypad",
},
@ -386,12 +393,13 @@ async def test_reconfigure_flow_flow_no_change(
"addr": "[02:08:02:01]",
"buttons": [
{
"led": True,
"name": "Morning",
"number": 1,
"release_delay": None,
},
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"name": "Foyer Keypad",
}
@ -492,12 +500,13 @@ async def test_options_add_remove_light_flow(
"addr": "[02:08:02:01]",
"buttons": [
{
"led": True,
"name": "Morning",
"number": 1,
"release_delay": None,
},
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"name": "Foyer Keypad",
}
@ -543,12 +552,13 @@ async def test_options_add_remove_light_flow(
"addr": "[02:08:02:01]",
"buttons": [
{
"led": True,
"name": "Morning",
"number": 1,
"release_delay": None,
},
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"name": "Foyer Keypad",
}
@ -602,12 +612,13 @@ async def test_options_add_remove_keypad_flow(
"addr": "[02:08:02:01]",
"buttons": [
{
"led": True,
"name": "Morning",
"number": 1,
"release_delay": None,
},
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"name": "Foyer Keypad",
},
@ -750,12 +761,13 @@ async def test_options_edit_light_no_lights_flow(
"addr": "[02:08:02:01]",
"buttons": [
{
"led": True,
"name": "Morning",
"number": 1,
"release_delay": None,
},
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"name": "Foyer Keypad",
}
@ -801,6 +813,7 @@ async def test_options_add_button_flow(
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
@ -836,6 +849,7 @@ async def test_options_add_button_flow(
CONF_NAME: "Dim down",
CONF_NUMBER: 4,
CONF_RELEASE_DELAY: 0.2,
CONF_LED: True,
},
)
await hass.async_block_till_done()
@ -850,13 +864,15 @@ async def test_options_add_button_flow(
"addr": "[02:08:02:01]",
"buttons": [
{
"led": True,
"name": "Morning",
"number": 1,
"release_delay": None,
},
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
{
"led": True,
"name": "Dim down",
"number": 4,
"release_delay": 0.2,
@ -871,6 +887,7 @@ async def test_options_add_button_flow(
await hass.async_block_till_done()
# Check the new entities were added
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 3
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 4
@ -881,6 +898,7 @@ async def test_options_add_button_flow_duplicate(
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
@ -930,6 +948,7 @@ async def test_options_edit_button_flow(
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
@ -974,6 +993,7 @@ async def test_options_edit_button_flow(
result["flow_id"],
user_input={
CONF_RELEASE_DELAY: 0,
CONF_LED: False,
},
)
await hass.async_block_till_done()
@ -988,12 +1008,13 @@ async def test_options_edit_button_flow(
"addr": "[02:08:02:01]",
"buttons": [
{
"led": False,
"name": "Morning",
"number": 1,
"release_delay": 0.0,
},
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"name": "Foyer Keypad",
}
@ -1004,6 +1025,7 @@ async def test_options_edit_button_flow(
await hass.async_block_till_done()
# Check the new entities were added
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3
@ -1061,8 +1083,8 @@ async def test_options_remove_button_flow(
{
"addr": "[02:08:02:01]",
"buttons": [
{"name": "Relax", "number": 2, "release_delay": None},
{"name": "Dim up", "number": 3, "release_delay": 0.2},
{"led": True, "name": "Relax", "number": 2, "release_delay": None},
{"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2},
],
"name": "Foyer Keypad",
}
@ -1073,4 +1095,5 @@ async def test_options_remove_button_flow(
await hass.async_block_till_done()
# Check the entities were removed
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2