mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add light platform to qbus (#136168)
* Add light platform * Add on/off for light * Renamed add_entities to async_add_entities * Revert qbusmqttapi bump * Align dependency version * Use AddConfigEntryEntitiesCallback * Use AddConfigEntryEntitiesCallback
This commit is contained in:
parent
5dfd358fc9
commit
5d851b6a56
@ -5,7 +5,10 @@ from typing import Final
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN: Final = "qbus"
|
||||
PLATFORMS: list[Platform] = [Platform.SWITCH]
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.LIGHT,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
CONF_SERIAL_NUMBER: Final = "serial"
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
"""Base class for Qbus entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
import re
|
||||
|
||||
from qbusmqttapi.discovery import QbusMqttOutput
|
||||
@ -10,12 +13,36 @@ from qbusmqttapi.state import QbusMqttState
|
||||
from homeassistant.components.mqtt import ReceiveMessage, client as mqtt
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import QbusControllerCoordinator
|
||||
|
||||
_REFID_REGEX = re.compile(r"^\d+\/(\d+(?:\/\d+)?)$")
|
||||
|
||||
|
||||
def add_new_outputs(
|
||||
coordinator: QbusControllerCoordinator,
|
||||
added_outputs: list[QbusMqttOutput],
|
||||
filter_fn: Callable[[QbusMqttOutput], bool],
|
||||
entity_type: type[QbusEntity],
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Call async_add_entities for new outputs."""
|
||||
|
||||
added_ref_ids = {k.ref_id for k in added_outputs}
|
||||
|
||||
new_outputs = [
|
||||
output
|
||||
for output in coordinator.data
|
||||
if filter_fn(output) and output.ref_id not in added_ref_ids
|
||||
]
|
||||
|
||||
if new_outputs:
|
||||
added_outputs.extend(new_outputs)
|
||||
async_add_entities([entity_type(output) for output in new_outputs])
|
||||
|
||||
|
||||
def format_ref_id(ref_id: str) -> str | None:
|
||||
"""Format the Qbus ref_id."""
|
||||
matches: list[str] = re.findall(_REFID_REGEX, ref_id)
|
||||
|
110
homeassistant/components/qbus/light.py
Normal file
110
homeassistant/components/qbus/light.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""Support for Qbus light."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from qbusmqttapi.discovery import QbusMqttOutput
|
||||
from qbusmqttapi.state import QbusMqttAnalogState, StateType
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.components.mqtt import ReceiveMessage
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.color import brightness_to_value, value_to_brightness
|
||||
|
||||
from .coordinator import QbusConfigEntry
|
||||
from .entity import QbusEntity, add_new_outputs
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: QbusConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up light entities."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
added_outputs: list[QbusMqttOutput] = []
|
||||
|
||||
def _check_outputs() -> None:
|
||||
add_new_outputs(
|
||||
coordinator,
|
||||
added_outputs,
|
||||
lambda output: output.type == "analog",
|
||||
QbusLight,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
_check_outputs()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))
|
||||
|
||||
|
||||
class QbusLight(QbusEntity, LightEntity):
|
||||
"""Representation of a Qbus light entity."""
|
||||
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
|
||||
def __init__(self, mqtt_output: QbusMqttOutput) -> None:
|
||||
"""Initialize light entity."""
|
||||
|
||||
super().__init__(mqtt_output)
|
||||
|
||||
self._set_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
|
||||
percentage: int | None = None
|
||||
on: bool | None = None
|
||||
|
||||
state = QbusMqttAnalogState(id=self._mqtt_output.id)
|
||||
|
||||
if brightness is None:
|
||||
on = True
|
||||
|
||||
state.type = StateType.ACTION
|
||||
state.write_on_off(on)
|
||||
else:
|
||||
percentage = round(brightness_to_value((1, 100), brightness))
|
||||
|
||||
state.type = StateType.STATE
|
||||
state.write_percentage(percentage)
|
||||
|
||||
await self._async_publish_output_state(state)
|
||||
self._set_state(percentage=percentage, on=on)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
state = QbusMqttAnalogState(id=self._mqtt_output.id, type=StateType.ACTION)
|
||||
state.write_on_off(on=False)
|
||||
|
||||
await self._async_publish_output_state(state)
|
||||
self._set_state(on=False)
|
||||
|
||||
async def _state_received(self, msg: ReceiveMessage) -> None:
|
||||
output = self._message_factory.parse_output_state(
|
||||
QbusMqttAnalogState, msg.payload
|
||||
)
|
||||
|
||||
if output is not None:
|
||||
percentage = round(output.read_percentage())
|
||||
self._set_state(percentage=percentage)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def _set_state(
|
||||
self, *, percentage: int | None = None, on: bool | None = None
|
||||
) -> None:
|
||||
if percentage is None:
|
||||
# When turning on without brightness, we don't know the desired
|
||||
# brightness. It will be set during _state_received().
|
||||
if on is True:
|
||||
self._attr_is_on = True
|
||||
else:
|
||||
self._attr_is_on = False
|
||||
self._attr_brightness = 0
|
||||
else:
|
||||
self._attr_is_on = percentage > 0
|
||||
self._attr_brightness = value_to_brightness((1, 100), percentage)
|
@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import QbusConfigEntry
|
||||
from .entity import QbusEntity
|
||||
from .entity import QbusEntity, add_new_outputs
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@ -19,26 +19,21 @@ PARALLEL_UPDATES = 0
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: QbusConfigEntry,
|
||||
add_entities: AddConfigEntryEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switch entities."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
added_outputs: list[QbusMqttOutput] = []
|
||||
|
||||
# Local function that calls add_entities for new entities
|
||||
def _check_outputs() -> None:
|
||||
added_output_ids = {k.id for k in added_outputs}
|
||||
|
||||
new_outputs = [
|
||||
item
|
||||
for item in coordinator.data
|
||||
if item.type == "onoff" and item.id not in added_output_ids
|
||||
]
|
||||
|
||||
if new_outputs:
|
||||
added_outputs.extend(new_outputs)
|
||||
add_entities([QbusSwitch(output) for output in new_outputs])
|
||||
add_new_outputs(
|
||||
coordinator,
|
||||
added_outputs,
|
||||
lambda output: output.type == "onoff",
|
||||
QbusSwitch,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
_check_outputs()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))
|
||||
@ -49,10 +44,7 @@ class QbusSwitch(QbusEntity, SwitchEntity):
|
||||
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mqtt_output: QbusMqttOutput,
|
||||
) -> None:
|
||||
def __init__(self, mqtt_output: QbusMqttOutput) -> None:
|
||||
"""Initialize switch entity."""
|
||||
|
||||
super().__init__(mqtt_output)
|
||||
|
@ -42,6 +42,29 @@
|
||||
"write": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "UL15",
|
||||
"location": "Media room",
|
||||
"locationId": 0,
|
||||
"name": "MEDIA ROOM",
|
||||
"originalName": "MEDIA ROOM",
|
||||
"refId": "000001/28",
|
||||
"type": "analog",
|
||||
"actions": {
|
||||
"off": null,
|
||||
"on": null
|
||||
},
|
||||
"properties": {
|
||||
"value": {
|
||||
"max": 100,
|
||||
"min": 5,
|
||||
"read": true,
|
||||
"step": 0.1,
|
||||
"type": "number",
|
||||
"write": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
118
tests/components/qbus/test_light.py
Normal file
118
tests/components/qbus/test_light.py
Normal file
@ -0,0 +1,118 @@
|
||||
"""Test Qbus light entities."""
|
||||
|
||||
import json
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
|
||||
from .const import TOPIC_CONFIG
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_mqtt_message
|
||||
from tests.typing import MqttMockHAClient
|
||||
|
||||
# 186 = 73% (rounded)
|
||||
_BRIGHTNESS = 186
|
||||
_BRIGHTNESS_PCT = 73
|
||||
|
||||
_PAYLOAD_LIGHT_STATE_ON = '{"id":"UL15","properties":{"value":60},"type":"state"}'
|
||||
_PAYLOAD_LIGHT_STATE_BRIGHTNESS = (
|
||||
'{"id":"UL15","properties":{"value":' + str(_BRIGHTNESS_PCT) + '},"type":"state"}'
|
||||
)
|
||||
_PAYLOAD_LIGHT_STATE_OFF = '{"id":"UL15","properties":{"value":0},"type":"state"}'
|
||||
|
||||
_PAYLOAD_LIGHT_SET_STATE_ON = '{"id": "UL15", "type": "action", "action": "on"}'
|
||||
_PAYLOAD_LIGHT_SET_STATE_BRIGHTNESS = (
|
||||
'{"id": "UL15", "type": "state", "properties": {"value": '
|
||||
+ str(_BRIGHTNESS_PCT)
|
||||
+ "}}"
|
||||
)
|
||||
_PAYLOAD_LIGHT_SET_STATE_OFF = '{"id": "UL15", "type": "action", "action": "off"}'
|
||||
|
||||
_TOPIC_LIGHT_STATE = "cloudapp/QBUSMQTTGW/UL1/UL15/state"
|
||||
_TOPIC_LIGHT_SET_STATE = "cloudapp/QBUSMQTTGW/UL1/UL15/setState"
|
||||
|
||||
_LIGHT_ENTITY_ID = "light.media_room"
|
||||
|
||||
|
||||
async def test_light(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
payload_config: JsonObjectType,
|
||||
) -> None:
|
||||
"""Test turning on and off."""
|
||||
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_mqtt_message(hass, TOPIC_CONFIG, json.dumps(payload_config))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Switch ON
|
||||
mqtt_mock.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: _LIGHT_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mqtt_mock.async_publish.assert_called_once_with(
|
||||
_TOPIC_LIGHT_SET_STATE, _PAYLOAD_LIGHT_SET_STATE_ON, 0, False
|
||||
)
|
||||
|
||||
# Simulate response
|
||||
async_fire_mqtt_message(hass, _TOPIC_LIGHT_STATE, _PAYLOAD_LIGHT_STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(_LIGHT_ENTITY_ID).state == STATE_ON
|
||||
|
||||
# Set brightness
|
||||
mqtt_mock.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: _LIGHT_ENTITY_ID,
|
||||
ATTR_BRIGHTNESS: _BRIGHTNESS,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mqtt_mock.async_publish.assert_called_once_with(
|
||||
_TOPIC_LIGHT_SET_STATE, _PAYLOAD_LIGHT_SET_STATE_BRIGHTNESS, 0, False
|
||||
)
|
||||
|
||||
# Simulate response
|
||||
async_fire_mqtt_message(hass, _TOPIC_LIGHT_STATE, _PAYLOAD_LIGHT_STATE_BRIGHTNESS)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity = hass.states.get(_LIGHT_ENTITY_ID)
|
||||
assert entity.state == STATE_ON
|
||||
assert entity.attributes.get(ATTR_BRIGHTNESS) == _BRIGHTNESS
|
||||
|
||||
# Switch OFF
|
||||
mqtt_mock.reset_mock()
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: _LIGHT_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mqtt_mock.async_publish.assert_called_once_with(
|
||||
_TOPIC_LIGHT_SET_STATE, _PAYLOAD_LIGHT_SET_STATE_OFF, 0, False
|
||||
)
|
||||
|
||||
# Simulate response
|
||||
async_fire_mqtt_message(hass, _TOPIC_LIGHT_STATE, _PAYLOAD_LIGHT_STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(_LIGHT_ENTITY_ID).state == STATE_OFF
|
Loading…
x
Reference in New Issue
Block a user