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
4 changed files with 126 additions and 8 deletions
|
@ -1,9 +1,13 @@
|
|||
"""Base entity for the Fully Kiosk Browser integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
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.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
@ -54,3 +58,28 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit
|
|||
(CONNECTION_NETWORK_MAC, coordinator.data["Mac"])
|
||||
}
|
||||
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",
|
||||
"name": "Fully Kiosk Browser",
|
||||
"after_dependencies": ["mqtt"],
|
||||
"codeowners": ["@cgarwood"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
|
|
|
@ -10,7 +10,7 @@ from fullykiosk import FullyKiosk
|
|||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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 .const import DOMAIN
|
||||
|
@ -25,6 +25,8 @@ class FullySwitchEntityDescriptionMixin:
|
|||
on_action: Callable[[FullyKiosk], Any]
|
||||
off_action: Callable[[FullyKiosk], Any]
|
||||
is_on_fn: Callable[[dict[str, Any]], Any]
|
||||
mqtt_on_event: str | None
|
||||
mqtt_off_event: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -41,6 +43,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||
on_action=lambda fully: fully.startScreensaver(),
|
||||
off_action=lambda fully: fully.stopScreensaver(),
|
||||
is_on_fn=lambda data: data.get("isInScreensaver"),
|
||||
mqtt_on_event="onScreensaverStart",
|
||||
mqtt_off_event="onScreensaverStop",
|
||||
),
|
||||
FullySwitchEntityDescription(
|
||||
key="maintenance",
|
||||
|
@ -49,6 +53,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||
on_action=lambda fully: fully.enableLockedMode(),
|
||||
off_action=lambda fully: fully.disableLockedMode(),
|
||||
is_on_fn=lambda data: data.get("maintenanceMode"),
|
||||
mqtt_on_event=None,
|
||||
mqtt_off_event=None,
|
||||
),
|
||||
FullySwitchEntityDescription(
|
||||
key="kiosk",
|
||||
|
@ -57,6 +63,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||
on_action=lambda fully: fully.lockKiosk(),
|
||||
off_action=lambda fully: fully.unlockKiosk(),
|
||||
is_on_fn=lambda data: data.get("kioskLocked"),
|
||||
mqtt_on_event=None,
|
||||
mqtt_off_event=None,
|
||||
),
|
||||
FullySwitchEntityDescription(
|
||||
key="motion-detection",
|
||||
|
@ -65,6 +73,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||
on_action=lambda fully: fully.enableMotionDetection(),
|
||||
off_action=lambda fully: fully.disableMotionDetection(),
|
||||
is_on_fn=lambda data: data["settings"].get("motionDetection"),
|
||||
mqtt_on_event=None,
|
||||
mqtt_off_event=None,
|
||||
),
|
||||
FullySwitchEntityDescription(
|
||||
key="screenOn",
|
||||
|
@ -72,6 +82,8 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = (
|
|||
on_action=lambda fully: fully.screenOn(),
|
||||
off_action=lambda fully: fully.screenOff(),
|
||||
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)
|
||||
self.entity_description = description
|
||||
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
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the entity is on."""
|
||||
if (is_on := self.entity_description.is_on_fn(self.coordinator.data)) is None:
|
||||
return None
|
||||
return bool(is_on)
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
description = self.entity_description
|
||||
self._turned_on_subscription = await self.mqtt_subscribe(
|
||||
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:
|
||||
"""Turn the entity on."""
|
||||
|
@ -122,3 +148,19 @@ class FullySwitchEntity(FullyKioskEntity, SwitchEntity):
|
|||
"""Turn the entity off."""
|
||||
await self.entity_description.off_action(self.coordinator.fully)
|
||||
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.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(
|
||||
|
@ -86,6 +87,51 @@ async def test_switches(
|
|||
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):
|
||||
"""Call any service on entity."""
|
||||
return hass.services.async_call(
|
||||
|
|
Loading…
Add table
Reference in a new issue