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

* use async_update

* improve tests

* use async_image
This commit is contained in:
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/freebox/switch.py
homeassistant/components/fritz/common.py homeassistant/components/fritz/common.py
homeassistant/components/fritz/device_tracker.py homeassistant/components/fritz/device_tracker.py
homeassistant/components/fritz/image.py
homeassistant/components/fritz/services.py homeassistant/components/fritz/services.py
homeassistant/components/fritz/switch.py homeassistant/components/fritz/switch.py
homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/__init__.py

View File

@ -48,6 +48,7 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
_attr_content_type = "image/png" _attr_content_type = "image/png"
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_should_poll = True
def __init__( def __init__(
self, self,
@ -63,22 +64,24 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
super().__init__(avm_wrapper, device_friendly_name) super().__init__(avm_wrapper, device_friendly_name)
ImageEntity.__init__(self, hass) ImageEntity.__init__(self, hass)
async def async_added_to_hass(self) -> None: async def _fetch_image(self) -> bytes:
"""Set the update time.""" """Fetch the QR code from the Fritz!Box."""
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( qr_stream: BytesIO = await self.hass.async_add_executor_job(
self._avm_wrapper.fritz_guest_wifi.get_wifi_qr_code, "png" self._avm_wrapper.fritz_guest_wifi.get_wifi_qr_code, "png"
) )
qr_bytes = qr_stream.getvalue() qr_bytes = qr_stream.getvalue()
_LOGGER.debug("fetched %s bytes", len(qr_bytes)) _LOGGER.debug("fetched %s bytes", len(qr_bytes))
if self._current_qr_bytes is None: return qr_bytes
self._current_qr_bytes = 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: if self._current_qr_bytes != qr_bytes:
dt_now = dt_util.utcnow() dt_now = dt_util.utcnow()
@ -87,4 +90,6 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
self._current_qr_bytes = qr_bytes self._current_qr_bytes = qr_bytes
self.async_write_ha_state() 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: else:
self.call_action = self._call_action 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): def _call_action(self, service: str, action: str, **kwargs):
LOGGER.debug( LOGGER.debug(
"_call_action service: %s, action: %s, **kwargs: %s", "_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.""" """Tests for Fritz!Tools image platform."""
from datetime import timedelta
from http import HTTPStatus
from unittest.mock import patch
import pytest import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.fritz.const import DOMAIN
from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from .const import MOCK_FB_SERVICES, MOCK_USER_DATA 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 from tests.typing import ClientSessionGenerator
GUEST_WIFI_ENABLED: dict[str, dict] = { GUEST_WIFI_ENABLED: dict[str, dict] = {
"WLANConfiguration0": { "WLANConfiguration0": {},
"GetInfo": {
"NewEnable": True,
"NewSSID": "HomeWifi",
}
},
"WLANConfiguration1": { "WLANConfiguration1": {
"GetInfo": { "GetInfo": {
"NewEnable": True, "NewEnable": True,
"NewStatus": "Up",
"NewSSID": "GuestWifi", "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] = { GUEST_WIFI_DISABLED: dict[str, dict] = {
"WLANConfiguration0": { "WLANConfiguration0": {},
"GetInfo": { "WLANConfiguration1": {"GetInfo": {"NewEnable": False}},
"NewEnable": True,
"NewSSID": "HomeWifi",
}
},
"WLANConfiguration1": {
"GetInfo": {
"NewEnable": False,
"NewSSID": "GuestWifi",
}
},
} }
@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_ENABLED})]) @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: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
fc_class_mock, fc_class_mock,
fh_class_mock, fh_class_mock,
) -> None: ) -> None:
"""Test image entities.""" """Test image entity."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) # setup component with image platform only
entry.add_to_hass(hass) 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() await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED assert entry.state == ConfigEntryState.LOADED
images = hass.states.async_all(IMAGE_DOMAIN) # test image entity is generated as expected
assert len(images) == 1 states = hass.states.async_all(IMAGE_DOMAIN)
assert images[0].name == "Mock Title GuestWifi" 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_registry = async_get_entity_registry(hass)
entity_entry = entity_registry.async_get("image.mock_title_guestwifi") entity_entry = entity_registry.async_get("image.mock_title_guestwifi")
assert entity_entry.unique_id == "1c_ed_6f_12_34_11_guestwifi_qr_code" 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})]) @pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_DISABLED})])
async def test_image_guest_wifi_disabled( async def test_image_guest_wifi_disabled(