Fix qr code data update in AVM Fritz!Tools (#95470)

* use async_update

* improve tests

* use async_image
pull/95478/head
Michael 2023-06-28 19:57:03 +02:00 committed by GitHub
parent 79f1c86789
commit b64be798df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 41 deletions

View File

@ -393,7 +393,6 @@ 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

View File

@ -48,6 +48,7 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
_attr_content_type = "image/png"
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True
_attr_should_poll = True
def __init__(
self,
@ -63,22 +64,24 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
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."""
async def _fetch_image(self) -> bytes:
"""Fetch the QR code from the Fritz!Box."""
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
return qr_bytes
async def async_added_to_hass(self) -> None:
"""Fetch and set initial data and state."""
self._current_qr_bytes = await self._fetch_image()
self._attr_image_last_updated = dt_util.utcnow()
async def async_update(self) -> None:
"""Update the image entity data."""
qr_bytes = await self._fetch_image()
if self._current_qr_bytes != qr_bytes:
dt_now = dt_util.utcnow()
@ -87,4 +90,6 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
self._current_qr_bytes = qr_bytes
self.async_write_ha_state()
return qr_bytes
async def async_image(self) -> bytes | None:
"""Return bytes of image."""
return self._current_qr_bytes

View File

@ -43,6 +43,10 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods
else:
self.call_action = self._call_action
def override_services(self, services) -> None:
"""Overrire services data."""
self._services = services
def _call_action(self, service: str, action: str, **kwargs):
LOGGER.debug(
"_call_action service: %s, action: %s, **kwargs: %s",

View File

@ -0,0 +1,13 @@
# serializer version: 1
# name: test_image[fc_data0]
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf5IDATx\xda\xedVQ\x0eC!\x0c"\xbb@\xef\x7fKn\xe0\x00\xfd\xdb\xcf6\xf9|\xc6\xc4\xc6\x0f\xd2\x02\xadb},\xe2\xb9\xfb\xe5\x0e\xc0(\x18\xf2\x84/|\xaeo\xef\x847\xda\x14\x1af\x1c\xde\xe3\x19(X\tKxN\xb2\x87\x17j9\x1d<m\x01)\xbbU\xe1\xcf\xa2\x9eU\xd1\xd7\xcbe.\xcc\xf6\xd05\x7f\x02\x82\x1d\xb8\x1c\xdd\xd7\x1b\xef\t\x90\x13an\xf1b\x13P\xb9\x01\xac\xd4k\xee\x04\xa5.\xd1.\xe8+\x90\x88\x1b\x0e\x0b\xfe\x03\xd3 \xd4Y\xe0\xef\x10\xa7z\xe3\xe9F\x7f(?;\xc6\x80\x95\xfc\xe2\x13\x1ddC\x0fZ\x07\xec6f\xc3/.\x94i\xddi\xf8\x8f\x9b9k<\x8d\xf9\xeci`\xfb\xed\xf1R\x99/g\x9e\xaei\xcc\x830\xb7\xf6\x83\xd4\xf1_\x9e\x0f\xf7.*\xf3\xc0\xf6\x1b\x86\xbf\x12\xde\xac\xed\x16\xb0\xf4\xbe\x9dO\x02\xd0\xe1\x8f\xee^\x0f|v\xf4\x15 \x13\xaf\x8e\xff\x9e\x7f\xe2\x9fwo\x06\xf4\x81v\xeb\xb3\xcc\xc3\x00\x00\x00\x00IEND\xaeB`\x82'
# ---
# name: test_image_download[fc_data0]
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf5IDATx\xda\xedVQ\x0eC!\x0c"\xbb@\xef\x7fKn\xe0\x00\xfd\xdb\xcf6\xf9|\xc6\xc4\xc6\x0f\xd2\x02\xadb},\xe2\xb9\xfb\xe5\x0e\xc0(\x18\xf2\x84/|\xaeo\xef\x847\xda\x14\x1af\x1c\xde\xe3\x19(X\tKxN\xb2\x87\x17j9\x1d<m\x01)\xbbU\xe1\xcf\xa2\x9eU\xd1\xd7\xcbe.\xcc\xf6\xd05\x7f\x02\x82\x1d\xb8\x1c\xdd\xd7\x1b\xef\t\x90\x13an\xf1b\x13P\xb9\x01\xac\xd4k\xee\x04\xa5.\xd1.\xe8+\x90\x88\x1b\x0e\x0b\xfe\x03\xd3 \xd4Y\xe0\xef\x10\xa7z\xe3\xe9F\x7f(?;\xc6\x80\x95\xfc\xe2\x13\x1ddC\x0fZ\x07\xec6f\xc3/.\x94i\xddi\xf8\x8f\x9b9k<\x8d\xf9\xeci`\xfb\xed\xf1R\x99/g\x9e\xaei\xcc\x830\xb7\xf6\x83\xd4\xf1_\x9e\x0f\xf7.*\xf3\xc0\xf6\x1b\x86\xbf\x12\xde\xac\xed\x16\xb0\xf4\xbe\x9dO\x02\xd0\xe1\x8f\xee^\x0f|v\xf4\x15 \x13\xaf\x8e\xff\x9e\x7f\xe2\x9fwo\x06\xf4\x81v\xeb\xb3\xcc\xc3\x00\x00\x00\x00IEND\xaeB`\x82'
# ---
# name: test_image_entity[fc_data0]
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf5IDATx\xda\xedVQ\x0eC!\x0c"\xbb@\xef\x7fKn\xe0\x00\xfd\xdb\xcf6\xf9|\xc6\xc4\xc6\x0f\xd2\x02\xadb},\xe2\xb9\xfb\xe5\x0e\xc0(\x18\xf2\x84/|\xaeo\xef\x847\xda\x14\x1af\x1c\xde\xe3\x19(X\tKxN\xb2\x87\x17j9\x1d<m\x01)\xbbU\xe1\xcf\xa2\x9eU\xd1\xd7\xcbe.\xcc\xf6\xd05\x7f\x02\x82\x1d\xb8\x1c\xdd\xd7\x1b\xef\t\x90\x13an\xf1b\x13P\xb9\x01\xac\xd4k\xee\x04\xa5.\xd1.\xe8+\x90\x88\x1b\x0e\x0b\xfe\x03\xd3 \xd4Y\xe0\xef\x10\xa7z\xe3\xe9F\x7f(?;\xc6\x80\x95\xfc\xe2\x13\x1ddC\x0fZ\x07\xec6f\xc3/.\x94i\xddi\xf8\x8f\x9b9k<\x8d\xf9\xeci`\xfb\xed\xf1R\x99/g\x9e\xaei\xcc\x830\xb7\xf6\x83\xd4\xf1_\x9e\x0f\xf7.*\xf3\xc0\xf6\x1b\x86\xbf\x12\xde\xac\xed\x16\xb0\xf4\xbe\x9dO\x02\xd0\xe1\x8f\xee^\x0f|v\xf4\x15 \x13\xaf\x8e\xff\x9e\x7f\xe2\x9fwo\x06\xf4\x81v\xeb\xb3\xcc\xc3\x00\x00\x00\x00IEND\xaeB`\x82'
# ---
# name: test_image_update[fc_data0]
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf9IDATx\xda\xedV\xc1\r\xc40\x0cB\xb7\x80\xf7\xdf\x92\r\\\xb0\xfb\xeb\xe7\xaa\xf0l\xd4\xaaQ\x1e\xc8\x06L\x8a~,\xe2;{s\x06\xa0\xd8z9\xdb\xe6\x0f\xcf\xf5\xef\x99\xf0J\x0f\x85\x86*o\xcf\xf1\x04\x04\x1ak\xb6\x11<\x97\xa6\xa6\x83x&\xb32x\x86\xa4\xab\xeb\x08\x7f\x16\xf5^\x11}\xbd$\xb0\x80k=t\xcc\x9f\xfdg\xfa\xda\xe5\x1d\xe3\t\x8br_\xdb3\x85D}\x063u\x00\x03\xfd\xb6<\xe2\xeaL\xa2y<\xae\xcf\xe3!\x895\xbfL\xf07\x0eT]n7\xc3_{0\xd4\xefx:\xc0\x1f\xc6}\x9e\xb7\x84\x1e\xfb\x91\x0e\x12\x84\t=z\xd2t\x07\x8e\x1d\xc9\x03\xc7\xa9G\xb7\x12\xf3&0\x176\x19\x98\xc8g\x8b;\x88@\xc6\x7f\x93\xa9\xfbVD\xdf\x193\xde9\x1d\xd1\xc3\x9ev`E\xf2oo\xa3\xe1/\x847\xad\x8a?0t\xffN\xb4p\xf35\xf3\x7f\x80\xad\xafS\xf7\x1bD`D\x8f\xef\x9f\xf0\xe0\xec\x02\xa4\xc0\x83\x92\xcf\xf3\xf9a\x00\x00\x00\x00IEND\xaeB`\x82'
# ---

View File

@ -1,74 +1,156 @@
"""Tests for Fritz!Tools image platform."""
from datetime import timedelta
from http import HTTPStatus
from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.fritz.const import DOMAIN
from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
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 homeassistant.util.dt import utcnow
from .const import MOCK_FB_SERVICES, MOCK_USER_DATA
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import ClientSessionGenerator
GUEST_WIFI_ENABLED: dict[str, dict] = {
"WLANConfiguration0": {
"GetInfo": {
"NewEnable": True,
"NewSSID": "HomeWifi",
}
},
"WLANConfiguration0": {},
"WLANConfiguration1": {
"GetInfo": {
"NewEnable": True,
"NewStatus": "Up",
"NewSSID": "GuestWifi",
}
"NewBeaconType": "11iandWPA3",
"NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3",
"NewStandard": "ax",
"NewBSSID": "1C:ED:6F:12:34:13",
},
"GetSSID": {
"NewSSID": "GuestWifi",
},
"GetSecurityKeys": {"NewKeyPassphrase": "1234567890"},
},
}
GUEST_WIFI_CHANGED: dict[str, dict] = {
"WLANConfiguration0": {},
"WLANConfiguration1": {
"GetInfo": {
"NewEnable": True,
"NewStatus": "Up",
"NewSSID": "GuestWifi",
"NewBeaconType": "11iandWPA3",
"NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3",
"NewStandard": "ax",
"NewBSSID": "1C:ED:6F:12:34:13",
},
"GetSSID": {
"NewSSID": "GuestWifi",
},
"GetSecurityKeys": {"NewKeyPassphrase": "abcdefghij"},
},
}
GUEST_WIFI_DISABLED: dict[str, dict] = {
"WLANConfiguration0": {
"GetInfo": {
"NewEnable": True,
"NewSSID": "HomeWifi",
}
},
"WLANConfiguration1": {
"GetInfo": {
"NewEnable": False,
"NewSSID": "GuestWifi",
}
},
"WLANConfiguration0": {},
"WLANConfiguration1": {"GetInfo": {"NewEnable": False}},
}
@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_ENABLED})])
async def test_image_entities_initialized(
async def test_image_entity(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
fc_class_mock,
fh_class_mock,
) -> None:
"""Test image entities."""
"""Test image entity."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
entry.add_to_hass(hass)
# setup component with image platform only
with patch(
"homeassistant.components.fritz.PLATFORMS",
[Platform.IMAGE],
):
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
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"
# test image entity is generated as expected
states = hass.states.async_all(IMAGE_DOMAIN)
assert len(states) == 1
state = states[0]
assert state.name == "Mock Title GuestWifi"
assert state.entity_id == "image.mock_title_guestwifi"
access_token = state.attributes["access_token"]
assert state.attributes == {
"access_token": access_token,
"entity_picture": f"/api/image_proxy/image.mock_title_guestwifi?token={access_token}",
"friendly_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"
# test image download
client = await hass_client()
resp = await client.get("/api/image_proxy/image.mock_title_guestwifi")
assert resp.status == HTTPStatus.OK
body = await resp.read()
assert body == snapshot
@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_ENABLED})])
async def test_image_update(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
fc_class_mock,
fh_class_mock,
) -> None:
"""Test image update."""
# setup component with image platform only
with patch(
"homeassistant.components.fritz.PLATFORMS",
[Platform.IMAGE],
):
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
client = await hass_client()
resp = await client.get("/api/image_proxy/image.mock_title_guestwifi")
resp_body = await resp.read()
assert resp.status == HTTPStatus.OK
fc_class_mock().override_services({**MOCK_FB_SERVICES, **GUEST_WIFI_CHANGED})
async_fire_time_changed(hass, utcnow() + timedelta(seconds=60))
await hass.async_block_till_done()
resp = await client.get("/api/image_proxy/image.mock_title_guestwifi")
resp_body_new = await resp.read()
assert resp_body != resp_body_new
assert resp_body_new == snapshot
@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_DISABLED})])
async def test_image_guest_wifi_disabled(