Add image platform to devolo_home_network (#98036)

This commit is contained in:
Guido Schmitz 2023-08-28 14:55:49 +02:00 committed by GitHub
parent 3f0a8b7a56
commit 660167cb1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 250 additions and 2 deletions

View file

@ -213,6 +213,7 @@ def platforms(device: Device) -> set[Platform]:
supported_platforms.add(Platform.BINARY_SENSOR) supported_platforms.add(Platform.BINARY_SENSOR)
if device.device and "wifi1" in device.device.features: if device.device and "wifi1" in device.device.features:
supported_platforms.add(Platform.DEVICE_TRACKER) supported_platforms.add(Platform.DEVICE_TRACKER)
supported_platforms.add(Platform.IMAGE)
if device.device and "update" in device.device.features: if device.device and "update" in device.device.features:
supported_platforms.add(Platform.UPDATE) supported_platforms.add(Platform.UPDATE)
return supported_platforms return supported_platforms

View file

@ -21,6 +21,7 @@ CONNECTED_PLC_DEVICES = "connected_plc_devices"
CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_TO_ROUTER = "connected_to_router"
CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients"
IDENTIFY = "identify" IDENTIFY = "identify"
IMAGE_GUEST_WIFI = "image_guest_wifi"
NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks"
PAIRING = "pairing" PAIRING = "pairing"
REGULAR_FIRMWARE = "regular_firmware" REGULAR_FIRMWARE = "regular_firmware"

View 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)

View file

@ -48,6 +48,11 @@
"name": "Start WPS" "name": "Start WPS"
} }
}, },
"image": {
"image_guest_wifi": {
"name": "Guest Wifi credentials as QR code"
}
},
"sensor": { "sensor": {
"connected_plc_devices": { "connected_plc_devices": {
"name": "Connected PLC devices" "name": "Connected PLC devices"

View file

@ -92,6 +92,13 @@ GUEST_WIFI = WifiGuestAccessGet(
remaining_duration=0, remaining_duration=0,
) )
GUEST_WIFI_CHANGED = WifiGuestAccessGet(
ssid="devolo-guest-930",
key="HMANPGAS",
enabled=False,
remaining_duration=0,
)
NEIGHBOR_ACCESS_POINTS = [ NEIGHBOR_ACCESS_POINTS = [
NeighborAPInfo( NeighborAPInfo(
mac_address="AA:BB:CC:DD:EE:FF", mac_address="AA:BB:CC:DD:EE:FF",

View file

@ -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'
# ---

View 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)

View file

@ -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.button import DOMAIN as BUTTON
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
from homeassistant.components.devolo_home_network.const import DOMAIN 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.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.components.update import DOMAIN as UPDATE 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", "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)], ["mock_nonwifi_device", (BINARY_SENSOR, BUTTON, SENSOR, SWITCH, UPDATE)],
], ],
) )