From 4daacf9c4bad16d2dfefe676426f3608c2465c9c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 27 Jun 2023 23:48:28 +0200 Subject: [PATCH] Add Guest WiFi QR-Code image entity to AVM Fritz!Tools (#95282) --- .coveragerc | 1 + homeassistant/components/fritz/const.py | 1 + homeassistant/components/fritz/image.py | 90 +++++++++++++++++++ homeassistant/components/fritz/manifest.json | 2 +- .../fritzbox_callmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/fritz/const.py | 1 - tests/components/fritz/test_image.py | 90 +++++++++++++++++++ 9 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/fritz/image.py create mode 100644 tests/components/fritz/test_image.py diff --git a/.coveragerc b/.coveragerc index a4faa19abed..cd787606432 100644 --- a/.coveragerc +++ b/.coveragerc @@ -393,6 +393,7 @@ omit = homeassistant/components/freebox/switch.py homeassistant/components/fritz/common.py homeassistant/components/fritz/device_tracker.py + homeassistant/components/fritz/image.py homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py homeassistant/components/fritzbox_callmonitor/__init__.py diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index d43ba2eda62..1ce21081f9c 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -31,6 +31,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, + Platform.IMAGE, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE, diff --git a/homeassistant/components/fritz/image.py b/homeassistant/components/fritz/image.py new file mode 100644 index 00000000000..b937e36aaef --- /dev/null +++ b/homeassistant/components/fritz/image.py @@ -0,0 +1,90 @@ +"""FRITZ image integration.""" + +from __future__ import annotations + +from io import BytesIO +import logging + +from homeassistant.components.image import ImageEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util, slugify + +from .common import AvmWrapper, FritzBoxBaseEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up guest WiFi QR code for device.""" + avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] + + guest_wifi_info = await hass.async_add_executor_job( + avm_wrapper.fritz_guest_wifi.get_info + ) + + if not guest_wifi_info.get("NewEnable"): + return + + async_add_entities( + [ + FritzGuestWifiQRImage( + avm_wrapper, entry.title, guest_wifi_info["NewSSID"], hass + ) + ] + ) + + +class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity): + """Implementation of the FritzBox guest wifi QR code image entity.""" + + _attr_content_type = "image/png" + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + + def __init__( + self, + avm_wrapper: AvmWrapper, + device_friendly_name: str, + ssid: str, + hass: HomeAssistant, + ) -> None: + """Initialize the image entity.""" + self._attr_name = ssid + self._attr_unique_id = slugify(f"{avm_wrapper.unique_id}-{ssid}-qr-code") + self._current_qr_bytes: bytes | None = None + super().__init__(avm_wrapper, device_friendly_name) + ImageEntity.__init__(self, hass) + + async def async_added_to_hass(self) -> None: + """Set the update time.""" + self._attr_image_last_updated = dt_util.utcnow() + + async def async_image(self) -> bytes: + """Return bytes of image.""" + qr_stream: BytesIO = await self.hass.async_add_executor_job( + self._avm_wrapper.fritz_guest_wifi.get_wifi_qr_code, "png" + ) + qr_bytes = qr_stream.getvalue() + + _LOGGER.debug("fetched %s bytes", len(qr_bytes)) + + if self._current_qr_bytes is None: + self._current_qr_bytes = qr_bytes + return qr_bytes + + if self._current_qr_bytes != qr_bytes: + dt_now = dt_util.utcnow() + _LOGGER.debug("qr code has changed, reset image last updated property") + self._attr_image_last_updated = dt_now + self._current_qr_bytes = qr_bytes + self.async_write_ha_state() + + return qr_bytes diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index b117218e23d..54419d5ae3f 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/fritz", "iot_class": "local_polling", "loggers": ["fritzconnection"], - "requirements": ["fritzconnection==1.12.0", "xmltodict==0.13.0"], + "requirements": ["fritzconnection[qr]==1.12.0", "xmltodict==0.13.0"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index cde955caa1e..d445c12e4da 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["fritzconnection"], - "requirements": ["fritzconnection==1.12.0"] + "requirements": ["fritzconnection[qr]==1.12.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 18c20ea11f7..a5a681624e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -818,7 +818,7 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.12.0 +fritzconnection[qr]==1.12.0 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7f49154fda..864a425f0be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -637,7 +637,7 @@ freebox-api==1.1.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.12.0 +fritzconnection[qr]==1.12.0 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index fb62e14bc6f..7a89aab1af1 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -195,7 +195,6 @@ MOCK_FB_SERVICES: dict[str, dict] = { }, } - MOCK_MESH_DATA = { "schema_version": "1.9", "nodes": [ diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py new file mode 100644 index 00000000000..7333095b9c7 --- /dev/null +++ b/tests/components/fritz/test_image.py @@ -0,0 +1,90 @@ +"""Tests for Fritz!Tools image platform.""" +import pytest + +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry +from homeassistant.setup import async_setup_component + +from .const import MOCK_FB_SERVICES, MOCK_USER_DATA + +from tests.common import MockConfigEntry +from tests.typing import ClientSessionGenerator + +GUEST_WIFI_ENABLED: dict[str, dict] = { + "WLANConfiguration0": { + "GetInfo": { + "NewEnable": True, + "NewSSID": "HomeWifi", + } + }, + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewSSID": "GuestWifi", + } + }, +} + +GUEST_WIFI_DISABLED: dict[str, dict] = { + "WLANConfiguration0": { + "GetInfo": { + "NewEnable": True, + "NewSSID": "HomeWifi", + } + }, + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": False, + "NewSSID": "GuestWifi", + } + }, +} + + +@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_ENABLED})]) +async def test_image_entities_initialized( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + fc_class_mock, + fh_class_mock, +) -> None: + """Test image entities.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + images = hass.states.async_all(IMAGE_DOMAIN) + assert len(images) == 1 + assert images[0].name == "Mock Title GuestWifi" + + entity_registry = async_get_entity_registry(hass) + entity_entry = entity_registry.async_get("image.mock_title_guestwifi") + + assert entity_entry.unique_id == "1c_ed_6f_12_34_11_guestwifi_qr_code" + + +@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_DISABLED})]) +async def test_image_guest_wifi_disabled( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + fc_class_mock, + fh_class_mock, +) -> None: + """Test image entities.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + images = hass.states.async_all(IMAGE_DOMAIN) + assert len(images) == 0