mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add support to fully_kiosk for hybrid local push/pull switches using MQTT (#89010)
* Support hybrid local push/pull switches using MQTT * Update homeassistant/components/fully_kiosk/entity.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Fix MQTT subscribe method --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
1144e33e68
commit
d460eadce0
@ -1,9 +1,13 @@
|
|||||||
"""Base entity for the Fully Kiosk Browser integration."""
|
"""Base entity for the Fully Kiosk Browser integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.const import ATTR_CONNECTIONS
|
from homeassistant.const import ATTR_CONNECTIONS
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, callback
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
@ -54,3 +58,28 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit
|
|||||||
(CONNECTION_NETWORK_MAC, coordinator.data["Mac"])
|
(CONNECTION_NETWORK_MAC, coordinator.data["Mac"])
|
||||||
}
|
}
|
||||||
self._attr_device_info = device_info
|
self._attr_device_info = device_info
|
||||||
|
|
||||||
|
async def mqtt_subscribe(
|
||||||
|
self, event: str | None, event_callback: CALLBACK_TYPE
|
||||||
|
) -> CALLBACK_TYPE | None:
|
||||||
|
"""Subscribe to MQTT for a given event."""
|
||||||
|
data = self.coordinator.data
|
||||||
|
if (
|
||||||
|
event is None
|
||||||
|
or not mqtt.mqtt_config_entry_enabled(self.hass)
|
||||||
|
or not data["settings"]["mqttEnabled"]
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def message_callback(message: mqtt.ReceiveMessage) -> None:
|
||||||
|
payload = json.loads(message.payload)
|
||||||
|
event_callback(**payload)
|
||||||
|
|
||||||
|
topic_template = data["settings"]["mqttEventTopic"]
|
||||||
|
topic = (
|
||||||
|
topic_template.replace("$appId", "fully")
|
||||||
|
.replace("$event", event)
|
||||||
|
.replace("$deviceId", data["deviceID"])
|
||||||
|
)
|
||||||
|
return await mqtt.async_subscribe(self.hass, topic, message_callback)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "fully_kiosk",
|
"domain": "fully_kiosk",
|
||||||
"name": "Fully Kiosk Browser",
|
"name": "Fully Kiosk Browser",
|
||||||
|
"after_dependencies": ["mqtt"],
|
||||||
"codeowners": ["@cgarwood"],
|
"codeowners": ["@cgarwood"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dhcp": [
|
"dhcp": [
|
||||||
|
@ -10,7 +10,7 @@ from fullykiosk import FullyKiosk
|
|||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -25,6 +25,8 @@ class FullySwitchEntityDescriptionMixin:
|
|||||||
on_action: Callable[[FullyKiosk], Any]
|
on_action: Callable[[FullyKiosk], Any]
|
||||||
off_action: Callable[[FullyKiosk], Any]
|
off_action: Callable[[FullyKiosk], Any]
|
||||||
is_on_fn: Callable[[dict[str, Any]], Any]
|
is_on_fn: Callable[[dict[str, Any]], Any]
|
||||||
|
mqtt_on_event: str | None
|
||||||
|
mqtt_off_event: str | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -41,6 +43,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||||||
on_action=lambda fully: fully.startScreensaver(),
|
on_action=lambda fully: fully.startScreensaver(),
|
||||||
off_action=lambda fully: fully.stopScreensaver(),
|
off_action=lambda fully: fully.stopScreensaver(),
|
||||||
is_on_fn=lambda data: data.get("isInScreensaver"),
|
is_on_fn=lambda data: data.get("isInScreensaver"),
|
||||||
|
mqtt_on_event="onScreensaverStart",
|
||||||
|
mqtt_off_event="onScreensaverStop",
|
||||||
),
|
),
|
||||||
FullySwitchEntityDescription(
|
FullySwitchEntityDescription(
|
||||||
key="maintenance",
|
key="maintenance",
|
||||||
@ -49,6 +53,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||||||
on_action=lambda fully: fully.enableLockedMode(),
|
on_action=lambda fully: fully.enableLockedMode(),
|
||||||
off_action=lambda fully: fully.disableLockedMode(),
|
off_action=lambda fully: fully.disableLockedMode(),
|
||||||
is_on_fn=lambda data: data.get("maintenanceMode"),
|
is_on_fn=lambda data: data.get("maintenanceMode"),
|
||||||
|
mqtt_on_event=None,
|
||||||
|
mqtt_off_event=None,
|
||||||
),
|
),
|
||||||
FullySwitchEntityDescription(
|
FullySwitchEntityDescription(
|
||||||
key="kiosk",
|
key="kiosk",
|
||||||
@ -57,6 +63,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||||||
on_action=lambda fully: fully.lockKiosk(),
|
on_action=lambda fully: fully.lockKiosk(),
|
||||||
off_action=lambda fully: fully.unlockKiosk(),
|
off_action=lambda fully: fully.unlockKiosk(),
|
||||||
is_on_fn=lambda data: data.get("kioskLocked"),
|
is_on_fn=lambda data: data.get("kioskLocked"),
|
||||||
|
mqtt_on_event=None,
|
||||||
|
mqtt_off_event=None,
|
||||||
),
|
),
|
||||||
FullySwitchEntityDescription(
|
FullySwitchEntityDescription(
|
||||||
key="motion-detection",
|
key="motion-detection",
|
||||||
@ -65,6 +73,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||||||
on_action=lambda fully: fully.enableMotionDetection(),
|
on_action=lambda fully: fully.enableMotionDetection(),
|
||||||
off_action=lambda fully: fully.disableMotionDetection(),
|
off_action=lambda fully: fully.disableMotionDetection(),
|
||||||
is_on_fn=lambda data: data["settings"].get("motionDetection"),
|
is_on_fn=lambda data: data["settings"].get("motionDetection"),
|
||||||
|
mqtt_on_event=None,
|
||||||
|
mqtt_off_event=None,
|
||||||
),
|
),
|
||||||
FullySwitchEntityDescription(
|
FullySwitchEntityDescription(
|
||||||
key="screenOn",
|
key="screenOn",
|
||||||
@ -72,6 +82,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||||||
on_action=lambda fully: fully.screenOn(),
|
on_action=lambda fully: fully.screenOn(),
|
||||||
off_action=lambda fully: fully.screenOff(),
|
off_action=lambda fully: fully.screenOff(),
|
||||||
is_on_fn=lambda data: data.get("screenOn"),
|
is_on_fn=lambda data: data.get("screenOn"),
|
||||||
|
mqtt_on_event="screenOn",
|
||||||
|
mqtt_off_event="screenOff",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -105,13 +117,27 @@ class FullySwitchEntity(FullyKioskEntity, SwitchEntity):
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}"
|
self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}"
|
||||||
|
self._turned_on_subscription: CALLBACK_TYPE | None = None
|
||||||
|
self._turned_off_subscription: CALLBACK_TYPE | None = None
|
||||||
|
|
||||||
@property
|
async def async_added_to_hass(self) -> None:
|
||||||
def is_on(self) -> bool | None:
|
"""When entity is added to hass."""
|
||||||
"""Return true if the entity is on."""
|
await super().async_added_to_hass()
|
||||||
if (is_on := self.entity_description.is_on_fn(self.coordinator.data)) is None:
|
description = self.entity_description
|
||||||
return None
|
self._turned_on_subscription = await self.mqtt_subscribe(
|
||||||
return bool(is_on)
|
description.mqtt_off_event, self._turn_off
|
||||||
|
)
|
||||||
|
self._turned_off_subscription = await self.mqtt_subscribe(
|
||||||
|
description.mqtt_on_event, self._turn_on
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Close MQTT subscriptions when removed."""
|
||||||
|
await super().async_will_remove_from_hass()
|
||||||
|
if self._turned_off_subscription is not None:
|
||||||
|
self._turned_off_subscription()
|
||||||
|
if self._turned_on_subscription is not None:
|
||||||
|
self._turned_on_subscription()
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the entity on."""
|
"""Turn the entity on."""
|
||||||
@ -122,3 +148,19 @@ class FullySwitchEntity(FullyKioskEntity, SwitchEntity):
|
|||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
await self.entity_description.off_action(self.coordinator.fully)
|
await self.entity_description.off_action(self.coordinator.fully)
|
||||||
await self.coordinator.async_refresh()
|
await self.coordinator.async_refresh()
|
||||||
|
|
||||||
|
def _turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Optimistically turn off."""
|
||||||
|
self._attr_is_on = False
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def _turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Optimistically turn on."""
|
||||||
|
self._attr_is_on = True
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated data from the coordinator."""
|
||||||
|
self._attr_is_on = bool(self.entity_description.is_on_fn(self.coordinator.data))
|
||||||
|
self.async_write_ha_state()
|
||||||
|
@ -7,7 +7,8 @@ from homeassistant.const import ATTR_ENTITY_ID
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, async_fire_mqtt_message
|
||||||
|
from tests.typing import MqttMockHAClient
|
||||||
|
|
||||||
|
|
||||||
async def test_switches(
|
async def test_switches(
|
||||||
@ -86,6 +87,51 @@ async def test_switches(
|
|||||||
assert device_entry.sw_version == "1.42.5"
|
assert device_entry.sw_version == "1.42.5"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_switches_mqtt_update(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_fully_kiosk: MagicMock,
|
||||||
|
mqtt_mock: MqttMockHAClient,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test push updates over MQTT."""
|
||||||
|
assert has_subscribed(mqtt_mock, "fully/event/onScreensaverStart/abcdef-123456")
|
||||||
|
assert has_subscribed(mqtt_mock, "fully/event/onScreensaverStop/abcdef-123456")
|
||||||
|
assert has_subscribed(mqtt_mock, "fully/event/screenOff/abcdef-123456")
|
||||||
|
assert has_subscribed(mqtt_mock, "fully/event/screenOn/abcdef-123456")
|
||||||
|
|
||||||
|
entity = hass.states.get("switch.amazon_fire_screensaver")
|
||||||
|
assert entity
|
||||||
|
assert entity.state == "off"
|
||||||
|
|
||||||
|
entity = hass.states.get("switch.amazon_fire_screen")
|
||||||
|
assert entity
|
||||||
|
assert entity.state == "on"
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "fully/event/onScreensaverStart/abcdef-123456", "{}")
|
||||||
|
entity = hass.states.get("switch.amazon_fire_screensaver")
|
||||||
|
assert entity.state == "on"
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "fully/event/onScreensaverStop/abcdef-123456", "{}")
|
||||||
|
entity = hass.states.get("switch.amazon_fire_screensaver")
|
||||||
|
assert entity.state == "off"
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "fully/event/screenOff/abcdef-123456", "{}")
|
||||||
|
entity = hass.states.get("switch.amazon_fire_screen")
|
||||||
|
assert entity.state == "off"
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "fully/event/screenOn/abcdef-123456", "{}")
|
||||||
|
entity = hass.states.get("switch.amazon_fire_screen")
|
||||||
|
assert entity.state == "on"
|
||||||
|
|
||||||
|
|
||||||
|
def has_subscribed(mqtt_mock: MqttMockHAClient, topic: str) -> bool:
|
||||||
|
"""Check if MQTT topic has subscription."""
|
||||||
|
for call in mqtt_mock.async_subscribe.call_args_list:
|
||||||
|
if call.args[0] == topic:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def call_service(hass, service, entity_id):
|
def call_service(hass, service, entity_id):
|
||||||
"""Call any service on entity."""
|
"""Call any service on entity."""
|
||||||
return hass.services.async_call(
|
return hass.services.async_call(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user