diff --git a/homeassistant/components/qbus/climate.py b/homeassistant/components/qbus/climate.py index 57d97c046b7..c6f234a14b7 100644 --- a/homeassistant/components/qbus/climate.py +++ b/homeassistant/components/qbus/climate.py @@ -57,6 +57,7 @@ async def async_setup_entry( class QbusClimate(QbusEntity, ClimateEntity): """Representation of a Qbus climate entity.""" + _attr_name = None _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ( ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/qbus/const.py b/homeassistant/components/qbus/const.py index 767a41f48cc..e679c4b9927 100644 --- a/homeassistant/components/qbus/const.py +++ b/homeassistant/components/qbus/const.py @@ -8,6 +8,7 @@ DOMAIN: Final = "qbus" PLATFORMS: list[Platform] = [ Platform.CLIMATE, Platform.LIGHT, + Platform.SCENE, Platform.SWITCH, ] diff --git a/homeassistant/components/qbus/coordinator.py b/homeassistant/components/qbus/coordinator.py index dd57a98787b..42e226c8e6a 100644 --- a/homeassistant/components/qbus/coordinator.py +++ b/homeassistant/components/qbus/coordinator.py @@ -105,6 +105,7 @@ class QbusControllerCoordinator(DataUpdateCoordinator[list[QbusMqttOutput]]): device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, self._controller.mac)}, identifiers={(DOMAIN, format_mac(self._controller.mac))}, manufacturer=MANUFACTURER, model="CTD3.x", diff --git a/homeassistant/components/qbus/entity.py b/homeassistant/components/qbus/entity.py index 4ab1913c4dc..70d469f9c93 100644 --- a/homeassistant/components/qbus/entity.py +++ b/homeassistant/components/qbus/entity.py @@ -54,34 +54,39 @@ def format_ref_id(ref_id: str) -> str | None: return None +def create_main_device_identifier(mqtt_output: QbusMqttOutput) -> tuple[str, str]: + """Create the identifier referring to the main device this output belongs to.""" + return (DOMAIN, format_mac(mqtt_output.device.mac)) + + class QbusEntity(Entity, ABC): """Representation of a Qbus entity.""" _attr_has_entity_name = True - _attr_name = None _attr_should_poll = False def __init__(self, mqtt_output: QbusMqttOutput) -> None: """Initialize the Qbus entity.""" + self._mqtt_output = mqtt_output + self._topic_factory = QbusMqttTopicFactory() self._message_factory = QbusMqttMessageFactory() + self._state_topic = self._topic_factory.get_output_state_topic( + mqtt_output.device.id, mqtt_output.id + ) ref_id = format_ref_id(mqtt_output.ref_id) self._attr_unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}" + # Create linked device self._attr_device_info = DeviceInfo( name=mqtt_output.name.title(), manufacturer=MANUFACTURER, identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")}, suggested_area=mqtt_output.location.title(), - via_device=(DOMAIN, format_mac(mqtt_output.device.mac)), - ) - - self._mqtt_output = mqtt_output - self._state_topic = self._topic_factory.get_output_state_topic( - mqtt_output.device.id, mqtt_output.id + via_device=create_main_device_identifier(mqtt_output), ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/qbus/light.py b/homeassistant/components/qbus/light.py index 3d2c763b8e3..654aab80ac7 100644 --- a/homeassistant/components/qbus/light.py +++ b/homeassistant/components/qbus/light.py @@ -43,6 +43,7 @@ async def async_setup_entry( class QbusLight(QbusEntity, LightEntity): """Representation of a Qbus light entity.""" + _attr_name = None _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_color_mode = ColorMode.BRIGHTNESS diff --git a/homeassistant/components/qbus/scene.py b/homeassistant/components/qbus/scene.py new file mode 100644 index 00000000000..9a9a1e2df83 --- /dev/null +++ b/homeassistant/components/qbus/scene.py @@ -0,0 +1,66 @@ +"""Support for Qbus scene.""" + +from typing import Any + +from qbusmqttapi.discovery import QbusMqttOutput +from qbusmqttapi.state import QbusMqttState, StateAction, StateType + +from homeassistant.components.mqtt import ReceiveMessage +from homeassistant.components.scene import Scene +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import QbusConfigEntry +from .entity import QbusEntity, add_new_outputs, create_main_device_identifier + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: QbusConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up scene entities.""" + + coordinator = entry.runtime_data + added_outputs: list[QbusMqttOutput] = [] + + def _check_outputs() -> None: + add_new_outputs( + coordinator, + added_outputs, + lambda output: output.type == "scene", + QbusScene, + async_add_entities, + ) + + _check_outputs() + entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) + + +class QbusScene(QbusEntity, Scene): + """Representation of a Qbus scene entity.""" + + def __init__(self, mqtt_output: QbusMqttOutput) -> None: + """Initialize scene entity.""" + + super().__init__(mqtt_output) + + # Add to main controller device + self._attr_device_info = DeviceInfo( + identifiers={create_main_device_identifier(mqtt_output)} + ) + self._attr_name = mqtt_output.name.title() + + async def async_activate(self, **kwargs: Any) -> None: + """Activate scene.""" + state = QbusMqttState( + id=self._mqtt_output.id, type=StateType.ACTION, action=StateAction.ACTIVE + ) + await self._async_publish_output_state(state) + + async def _state_received(self, msg: ReceiveMessage) -> None: + # Nothing to do + pass diff --git a/homeassistant/components/qbus/switch.py b/homeassistant/components/qbus/switch.py index e1feccf4450..c0e2b112bc5 100644 --- a/homeassistant/components/qbus/switch.py +++ b/homeassistant/components/qbus/switch.py @@ -42,6 +42,7 @@ async def async_setup_entry( class QbusSwitch(QbusEntity, SwitchEntity): """Representation of a Qbus switch entity.""" + _attr_name = None _attr_device_class = SwitchDeviceClass.SWITCH def __init__(self, mqtt_output: QbusMqttOutput) -> None: diff --git a/tests/components/qbus/fixtures/payload_config.json b/tests/components/qbus/fixtures/payload_config.json index fc204c975ad..3a9e845bc26 100644 --- a/tests/components/qbus/fixtures/payload_config.json +++ b/tests/components/qbus/fixtures/payload_config.json @@ -99,6 +99,19 @@ "write": true } } + }, + { + "id": "UL25", + "location": "Living", + "locationId": 0, + "name": "Watching TV", + "originalName": "Watching TV", + "refId": "000001/105/3", + "type": "scene", + "actions": { + "active": null + }, + "properties": {} } ] } diff --git a/tests/components/qbus/test_scene.py b/tests/components/qbus/test_scene.py new file mode 100644 index 00000000000..8fdf60ec502 --- /dev/null +++ b/tests/components/qbus/test_scene.py @@ -0,0 +1,45 @@ +"""Test Qbus scene entities.""" + +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from tests.common import async_fire_mqtt_message +from tests.typing import MqttMockHAClient + +_PAYLOAD_SCENE_STATE = '{"id":"UL25","properties":{"value":true},"type":"state"}' +_PAYLOAD_SCENE_ACTIVATE = '{"id": "UL25", "type": "action", "action": "active"}' + +_TOPIC_SCENE_STATE = "cloudapp/QBUSMQTTGW/UL1/UL25/state" +_TOPIC_SCENE_SET_STATE = "cloudapp/QBUSMQTTGW/UL1/UL25/setState" + +_SCENE_ENTITY_ID = "scene.ctd_000001_watching_tv" + + +async def test_scene( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + setup_integration: None, +) -> None: + """Test scene.""" + + assert hass.states.get(_SCENE_ENTITY_ID).state == STATE_UNKNOWN + + # Activate scene + mqtt_mock.reset_mock() + await hass.services.async_call( + SCENE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: _SCENE_ENTITY_ID}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + _TOPIC_SCENE_SET_STATE, _PAYLOAD_SCENE_ACTIVATE, 0, False + ) + + # Simulate response + async_fire_mqtt_message(hass, _TOPIC_SCENE_STATE, _PAYLOAD_SCENE_STATE) + await hass.async_block_till_done() + + assert hass.states.get(_SCENE_ENTITY_ID).state != STATE_UNKNOWN