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)
|
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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
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"
|
"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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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.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)],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue