Add image platform to devolo_home_network (#98036)
This commit is contained in:
parent
3f0a8b7a56
commit
660167cb1b
8 changed files with 250 additions and 2 deletions
|
@ -213,6 +213,7 @@ def platforms(device: Device) -> set[Platform]:
|
|||
supported_platforms.add(Platform.BINARY_SENSOR)
|
||||
if device.device and "wifi1" in device.device.features:
|
||||
supported_platforms.add(Platform.DEVICE_TRACKER)
|
||||
supported_platforms.add(Platform.IMAGE)
|
||||
if device.device and "update" in device.device.features:
|
||||
supported_platforms.add(Platform.UPDATE)
|
||||
return supported_platforms
|
||||
|
|
|
@ -21,6 +21,7 @@ CONNECTED_PLC_DEVICES = "connected_plc_devices"
|
|||
CONNECTED_TO_ROUTER = "connected_to_router"
|
||||
CONNECTED_WIFI_CLIENTS = "connected_wifi_clients"
|
||||
IDENTIFY = "identify"
|
||||
IMAGE_GUEST_WIFI = "image_guest_wifi"
|
||||
NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks"
|
||||
PAIRING = "pairing"
|
||||
REGULAR_FIRMWARE = "regular_firmware"
|
||||
|
|
100
homeassistant/components/devolo_home_network/image.py
Normal file
100
homeassistant/components/devolo_home_network/image.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
"""Platform for image integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from devolo_plc_api import Device, wifi_qr_code
|
||||
from devolo_plc_api.device_api import WifiGuestAccessGet
|
||||
|
||||
from homeassistant.components.image import ImageEntity, ImageEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import DOMAIN, IMAGE_GUEST_WIFI, SWITCH_GUEST_WIFI
|
||||
from .entity import DevoloCoordinatorEntity
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevoloImageRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
image_func: Callable[[WifiGuestAccessGet], bytes]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevoloImageEntityDescription(
|
||||
ImageEntityDescription, DevoloImageRequiredKeysMixin
|
||||
):
|
||||
"""Describes devolo image entity."""
|
||||
|
||||
|
||||
IMAGE_TYPES: dict[str, DevoloImageEntityDescription] = {
|
||||
IMAGE_GUEST_WIFI: DevoloImageEntityDescription(
|
||||
key=IMAGE_GUEST_WIFI,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
image_func=partial(wifi_qr_code, omitsize=True),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Get all devices and sensors and setup them via config entry."""
|
||||
device: Device = hass.data[DOMAIN][entry.entry_id]["device"]
|
||||
coordinators: dict[str, DataUpdateCoordinator[Any]] = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]["coordinators"]
|
||||
|
||||
entities: list[ImageEntity] = []
|
||||
entities.append(
|
||||
DevoloImageEntity(
|
||||
entry,
|
||||
coordinators[SWITCH_GUEST_WIFI],
|
||||
IMAGE_TYPES[IMAGE_GUEST_WIFI],
|
||||
device,
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class DevoloImageEntity(DevoloCoordinatorEntity[WifiGuestAccessGet], ImageEntity):
|
||||
"""Representation of a devolo image."""
|
||||
|
||||
_attr_content_type = "image/svg+xml"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: ConfigEntry,
|
||||
coordinator: DataUpdateCoordinator[WifiGuestAccessGet],
|
||||
description: DevoloImageEntityDescription,
|
||||
device: Device,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
self.entity_description: DevoloImageEntityDescription = description
|
||||
super().__init__(entry, coordinator, device)
|
||||
ImageEntity.__init__(self, coordinator.hass)
|
||||
self._attr_image_last_updated = dt_util.utcnow()
|
||||
self._data = self.coordinator.data
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if (
|
||||
self._data.ssid != self.coordinator.data.ssid
|
||||
or self._data.key != self.coordinator.data.key
|
||||
):
|
||||
self._data = self.coordinator.data
|
||||
self._attr_image_last_updated = dt_util.utcnow()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
async def async_image(self) -> bytes | None:
|
||||
"""Return bytes of image."""
|
||||
return self.entity_description.image_func(self.coordinator.data)
|
|
@ -48,6 +48,11 @@
|
|||
"name": "Start WPS"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"image_guest_wifi": {
|
||||
"name": "Guest Wifi credentials as QR code"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"connected_plc_devices": {
|
||||
"name": "Connected PLC devices"
|
||||
|
|
|
@ -92,6 +92,13 @@ GUEST_WIFI = WifiGuestAccessGet(
|
|||
remaining_duration=0,
|
||||
)
|
||||
|
||||
GUEST_WIFI_CHANGED = WifiGuestAccessGet(
|
||||
ssid="devolo-guest-930",
|
||||
key="HMANPGAS",
|
||||
enabled=False,
|
||||
remaining_duration=0,
|
||||
)
|
||||
|
||||
NEIGHBOR_ACCESS_POINTS = [
|
||||
NeighborAPInfo(
|
||||
mac_address="AA:BB:CC:DD:EE:FF",
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# serializer version: 1
|
||||
# name: test_guest_wifi_qr
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'image',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'image.mock_title_guest_wifi_credentials_as_qr_code',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Guest Wifi credentials as QR code',
|
||||
'platform': 'devolo_home_network',
|
||||
'supported_features': 0,
|
||||
'translation_key': 'image_guest_wifi',
|
||||
'unique_id': '1234567890_image_guest_wifi',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_guest_wifi_qr.1
|
||||
b'<?xml version="1.0" encoding="utf-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 37 37" class="segno"><path class="qrline" stroke="#000" d="M4 4.5h7m2 0h1m4 0h3m1 0h3m1 0h7m-29 1h1m5 0h1m2 0h2m1 0h1m2 0h1m4 0h1m1 0h1m5 0h1m-29 1h1m1 0h3m1 0h1m1 0h2m1 0h2m3 0h1m3 0h1m1 0h1m1 0h3m1 0h1m-29 1h1m1 0h3m1 0h1m1 0h1m1 0h2m2 0h1m2 0h1m2 0h1m1 0h1m1 0h3m1 0h1m-29 1h1m1 0h3m1 0h1m1 0h3m1 0h1m2 0h2m2 0h2m1 0h1m1 0h3m1 0h1m-29 1h1m5 0h1m1 0h2m3 0h6m1 0h1m1 0h1m5 0h1m-29 1h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7m-21 1h1m1 0h3m1 0h2m3 0h2m-21 1h1m1 0h5m2 0h2m1 0h1m1 0h2m1 0h1m4 0h5m-26 1h1m1 0h1m3 0h3m1 0h1m3 0h2m2 0h2m1 0h1m1 0h1m1 0h2m-27 1h4m1 0h1m2 0h2m1 0h2m1 0h2m3 0h4m1 0h1m-26 1h3m1 0h2m2 0h1m2 0h1m3 0h2m1 0h1m1 0h3m1 0h1m2 0h2m-28 1h1m2 0h1m1 0h2m2 0h1m1 0h1m1 0h1m2 0h3m3 0h4m-27 1h1m1 0h2m3 0h1m2 0h1m4 0h5m1 0h4m1 0h2m-28 1h3m3 0h7m2 0h1m4 0h1m2 0h1m2 0h1m-26 1h3m4 0h1m1 0h3m1 0h2m3 0h3m3 0h1m-25 1h4m1 0h1m3 0h4m3 0h3m3 0h1m1 0h1m1 0h2m-29 1h2m7 0h1m3 0h5m1 0h4m1 0h2m1 0h1m-28 1h1m1 0h2m1 0h4m1 0h1m2 0h1m1 0h1m2 0h1m4 0h2m-25 1h1m1 0h2m3 0h1m1 0h4m1 0h1m5 0h1m3 0h1m3 0h1m-29 1h1m4 0h6m2 0h1m2 0h11m-19 1h2m2 0h3m5 0h1m3 0h1m1 0h1m-27 1h7m6 0h1m1 0h1m1 0h4m1 0h1m1 0h1m1 0h1m-27 1h1m5 0h1m1 0h3m3 0h2m3 0h2m3 0h2m1 0h2m-29 1h1m1 0h3m1 0h1m1 0h3m1 0h1m2 0h4m1 0h6m1 0h2m-29 1h1m1 0h3m1 0h1m1 0h2m1 0h3m1 0h2m2 0h2m3 0h2m1 0h2m-29 1h1m1 0h3m1 0h1m1 0h1m1 0h2m1 0h4m1 0h3m1 0h6m-28 1h1m5 0h1m4 0h4m3 0h1m1 0h1m3 0h2m1 0h1m-28 1h7m1 0h2m3 0h1m2 0h4m1 0h2m1 0h2"/></svg>\n'
|
||||
# ---
|
96
tests/components/devolo_home_network/test_image.py
Normal file
96
tests/components/devolo_home_network/test_image.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
"""Tests for the devolo Home Network images."""
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from devolo_plc_api.exceptions.device import DeviceUnavailable
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.devolo_home_network.const import SHORT_UPDATE_INTERVAL
|
||||
from homeassistant.components.image import DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import configure_integration
|
||||
from .const import GUEST_WIFI_CHANGED
|
||||
from .mock import MockDevice
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_device")
|
||||
async def test_image_setup(hass: HomeAssistant) -> None:
|
||||
"""Test default setup of the image component."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass.states.get(f"{DOMAIN}.{device_name}_guest_wifi_credentials_as_qr_code")
|
||||
is not None
|
||||
)
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-01-13 12:00:00+00:00")
|
||||
async def test_guest_wifi_qr(
|
||||
hass: HomeAssistant,
|
||||
mock_device: MockDevice,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_client: ClientSessionGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test showing a QR code of the guest wifi credentials."""
|
||||
entry = configure_integration(hass)
|
||||
device_name = entry.title.replace(" ", "_").lower()
|
||||
state_key = f"{DOMAIN}.{device_name}_guest_wifi_credentials_as_qr_code"
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(state_key)
|
||||
assert state.name == "Mock Title Guest Wifi credentials as QR code"
|
||||
assert state.state == dt_util.utcnow().isoformat()
|
||||
assert entity_registry.async_get(state_key) == snapshot
|
||||
|
||||
client = await hass_client()
|
||||
resp = await client.get(f"/api/image_proxy/{state_key}")
|
||||
assert resp.status == HTTPStatus.OK
|
||||
body = await resp.read()
|
||||
assert body == snapshot
|
||||
|
||||
# Emulate device failure
|
||||
mock_device.device.async_get_wifi_guest_access.side_effect = DeviceUnavailable()
|
||||
freezer.move_to(dt_util.utcnow() + SHORT_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SHORT_UPDATE_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(state_key)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Emulate state change
|
||||
mock_device.device.async_get_wifi_guest_access = AsyncMock(
|
||||
return_value=GUEST_WIFI_CHANGED
|
||||
)
|
||||
freezer.move_to(dt_util.utcnow() + SHORT_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + SHORT_UPDATE_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(state_key)
|
||||
assert state is not None
|
||||
assert state.state == dt_util.utcnow().isoformat()
|
||||
|
||||
client = await hass_client()
|
||||
resp = await client.get(f"/api/image_proxy/{state_key}")
|
||||
assert resp.status == HTTPStatus.OK
|
||||
assert await resp.read() != body
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
|
@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
|
|||
from homeassistant.components.button import DOMAIN as BUTTON
|
||||
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
|
||||
from homeassistant.components.devolo_home_network.const import DOMAIN
|
||||
from homeassistant.components.image import DOMAIN as IMAGE
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||
from homeassistant.components.update import DOMAIN as UPDATE
|
||||
|
@ -87,9 +88,12 @@ async def test_hass_stop(hass: HomeAssistant, mock_device: MockDevice) -> None:
|
|||
[
|
||||
[
|
||||
"mock_device",
|
||||
(BINARY_SENSOR, BUTTON, DEVICE_TRACKER, SENSOR, SWITCH, UPDATE),
|
||||
(BINARY_SENSOR, BUTTON, DEVICE_TRACKER, IMAGE, SENSOR, SWITCH, UPDATE),
|
||||
],
|
||||
[
|
||||
"mock_repeater_device",
|
||||
(BUTTON, DEVICE_TRACKER, IMAGE, SENSOR, SWITCH, UPDATE),
|
||||
],
|
||||
["mock_repeater_device", (BUTTON, DEVICE_TRACKER, SENSOR, SWITCH, UPDATE)],
|
||||
["mock_nonwifi_device", (BINARY_SENSOR, BUTTON, SENSOR, SWITCH, UPDATE)],
|
||||
],
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue