From ee57aab167f9e741313356fe5a9502f43faf3add Mon Sep 17 00:00:00 2001 From: BambamNZ Date: Sat, 25 Jul 2026 10:04:49 +1200 Subject: [PATCH 01/16] refactor: Update RGB Idle Light to Light Entitity --- custom_components/panda_status/__init__.py | 8 + custom_components/panda_status/coordinator.py | 34 +++- custom_components/panda_status/light.py | 181 ++++++++++++++++++ custom_components/panda_status/switch.py | 60 ------ custom_components/panda_status/websocket.py | 9 +- 5 files changed, 222 insertions(+), 70 deletions(-) create mode 100644 custom_components/panda_status/light.py diff --git a/custom_components/panda_status/__init__.py b/custom_components/panda_status/__init__.py index 5a3c8f6..e5ef83e 100644 --- a/custom_components/panda_status/__init__.py +++ b/custom_components/panda_status/__init__.py @@ -7,6 +7,7 @@ from __future__ import annotations +from datetime import timedelta from typing import TYPE_CHECKING from homeassistant.const import CONF_URL, Platform @@ -25,6 +26,7 @@ Platform.SENSOR, Platform.SELECT, Platform.SWITCH, + Platform.LIGHT, ] @@ -38,6 +40,12 @@ async def async_setup_entry( hass=hass, logger=LOGGER, name=DOMAIN, + # Real polling, previously unset (see PR history). 30s turned out + # too aggressive for the device's embedded WS stack when combined + # with the 1s-per-connection timeout, causing frequent unavailable + # flaps - 60s plus the more forgiving timeout and single retry in + # the coordinator gives it more breathing room. + update_interval=timedelta(seconds=60), ) entry.runtime_data = PandaStatusData( client=PandaStatusWebSocket(url=entry.data[CONF_URL], session=None), diff --git a/custom_components/panda_status/coordinator.py b/custom_components/panda_status/coordinator.py index 6528f4b..fe42bb6 100644 --- a/custom_components/panda_status/coordinator.py +++ b/custom_components/panda_status/coordinator.py @@ -7,7 +7,11 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .websocket import PandaStatusWebsocketCommunicationError, PandaStatusWebsocketError +from .websocket import ( + PandaStatusWebsocketCommunicationError, + PandaStatusWebsocketError, + PandaStatusWebsocketTimeoutError, +) if TYPE_CHECKING: from .data import PandaStatusConfigEntry @@ -20,10 +24,24 @@ class PandaStatusDataUpdateCoordinator(DataUpdateCoordinator): config_entry: PandaStatusConfigEntry async def _async_update_data(self) -> Any: - """Update data via library.""" - try: - return await self.config_entry.runtime_data.client.async_get_data() - except PandaStatusWebsocketCommunicationError as exception: - raise ConfigEntryNotReady(exception) from exception - except PandaStatusWebsocketError as exception: - raise UpdateFailed(exception) from exception + """Update data via library. + + A single slow/timed-out poll is retried once before being treated + as a real failure. With periodic polling now running continuously + rather than only right after user actions, occasionally catching + the device's embedded WS stack mid-task is expected background + noise, not a genuine unavailability - retrying once avoids flapping + entities to unavailable over what is usually just a slow beat. + """ + attempts = 2 + for attempt in range(attempts): + try: + return await self.config_entry.runtime_data.client.async_get_data() + except PandaStatusWebsocketTimeoutError as exception: + if attempt == attempts - 1: + raise UpdateFailed(exception) from exception + except PandaStatusWebsocketCommunicationError as exception: + raise ConfigEntryNotReady(exception) from exception + except PandaStatusWebsocketError as exception: + raise UpdateFailed(exception) from exception + return None # pragma: no cover - unreachable, loop always returns or raises diff --git a/custom_components/panda_status/light.py b/custom_components/panda_status/light.py new file mode 100644 index 0000000..c77d558 --- /dev/null +++ b/custom_components/panda_status/light.py @@ -0,0 +1,181 @@ +"""Light platform for Panda Status integration.""" + +from __future__ import annotations + +import asyncio +import json +from typing import TYPE_CHECKING, Any, ClassVar + +from custom_components.panda_status import tools +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ColorMode, + LightEntity, + LightEntityDescription, +) +from homeassistant.core import callback + +from .entity import PandaStatusEntity + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from .coordinator import PandaStatusDataUpdateCoordinator + from .data import PandaStatusConfigEntry + + +async def async_setup_entry( + hass: HomeAssistant, # noqa: ARG001 + entry: PandaStatusConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the panda_status light platform.""" + coordinator = entry.runtime_data.coordinator + async_add_entities( + [ + PandaStatusRGBIdleLight( + coordinator=coordinator, + entity_description=LightEntityDescription( + key="rgb_idle_light", + name="RGB Idle Light", + icon="mdi:led-strip-variant", + ), + ), + ] + ) + + +class PandaStatusRGBIdleLight(PandaStatusEntity, LightEntity): + """Representation of the RGB Idle Light. + + Replaces the old rgb_idle_light switch. The device exposes a real + on/off flag (led.on / settings.on) that is entirely separate from + brightness (led.brightness / settings.list2[current_mode].brightness), + so this is modelled as a brightness-only light rather than a switch + that faked "on" by slamming brightness to 100. + + Confirmed via direct testing against the device: on/off must be sent + as a single atomic payload including settings.current_mode - the + device will not turn off without it. + """ + + _attr_color_mode = ColorMode.BRIGHTNESS + _attr_supported_color_modes: ClassVar[set[ColorMode]] = {ColorMode.BRIGHTNESS} + + def __init__( + self, + coordinator: PandaStatusDataUpdateCoordinator, + entity_description: LightEntityDescription, + ) -> None: + """ + Initialize the RGB Idle Light entity. + + Args: + coordinator: The data update coordinator for panda_status. + entity_description: Description of the light entity. + + """ + super().__init__(coordinator, entity_description) + self.entity_description = entity_description + self._attr_is_on = self._get_on_state() + self._attr_brightness = self._get_brightness() + + def _get_on_state(self) -> bool | None: + """Get the current on/off state from the real led.on flag.""" + on_state = tools.extract_value(self.coordinator.data, "led.on") + if on_state is not None: + return bool(on_state) + return None + + def _get_brightness(self) -> int | None: + """Get current brightness, converted from the device's 0-100 scale.""" + device_brightness = tools.extract_value( + self.coordinator.data, "led.brightness" + ) + if device_brightness is None: + return None + return round(device_brightness / 100 * 255) + + def _current_mode(self) -> int: + """Get the currently active rgb_info_mode, defaulting to 0.""" + mode = tools.extract_value(self.coordinator.data, "settings.current_mode") + return mode if mode is not None else 0 + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = self._get_on_state() + self._attr_brightness = self._get_brightness() + self.async_write_ha_state() + + async def _async_reconcile_after_delay(self) -> None: + """Reconcile with the device's real state after it has had time to settle. + + Each command opens a brand new WebSocket connection (see + websocket.py) with no guarantee the device has finished applying it + before that connection closes. Refreshing immediately after sending + a command can therefore read back stale data and stomp the + optimistic state set in async_turn_on/async_turn_off. Waiting a + moment first, and running this as a background task rather than + blocking the service call, avoids that without making the UI feel + laggy. + """ + await asyncio.sleep(1) + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the RGB Idle Light, optionally setting brightness.""" + settings: dict[str, Any] = { + "on": True, + "current_mode": self._current_mode(), + } + led: dict[str, Any] = {"on": True} + + device_brightness: int | None = None + if ATTR_BRIGHTNESS in kwargs: + # HA brightness is 0-255, device wants a 1-100 percentage. + # Clamp to 1 rather than 0 - a 0% brightness "on" call would + # visually look off but leave the device in an on/on state + # mismatched with what the user asked for; turn_off handles + # actually turning it off. + # + # Confirmed via WS sniffing of the device's own web UI: setting + # brightness requires BOTH settings.rgb_info_brightness (as a + # string) AND led.brightness (as an int) - the device does not + # propagate the former into the latter on its own, and + # led.brightness is the field this entity reads back for + # display, so sending only the settings side left the UI + # permanently stale regardless of how long a refresh waited. + device_brightness = max(1, round(kwargs[ATTR_BRIGHTNESS] / 255 * 100)) + settings["rgb_info_brightness"] = str(device_brightness) + led["brightness"] = device_brightness + + payload = json.dumps({"settings": settings, "led": led}) + await self.coordinator.config_entry.runtime_data.client.async_send(payload) + + # Update local state optimistically - this is the value the UI + # shows immediately, ahead of the delayed reconciliation below. + self._attr_is_on = True + if device_brightness is not None: + self._attr_brightness = round(device_brightness / 100 * 255) + self.async_write_ha_state() + + self.hass.async_create_task(self._async_reconcile_after_delay()) + + async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 + """Turn off the RGB Idle Light without touching brightness.""" + payload = json.dumps( + { + "settings": {"on": False, "current_mode": self._current_mode()}, + "led": {"on": False}, + } + ) + await self.coordinator.config_entry.runtime_data.client.async_send(payload) + + # See async_turn_on for why this is set optimistically rather than + # waiting solely on a refresh. + self._attr_is_on = False + self.async_write_ha_state() + + self.hass.async_create_task(self._async_reconcile_after_delay()) \ No newline at end of file diff --git a/custom_components/panda_status/switch.py b/custom_components/panda_status/switch.py index f2e535e..0ac0833 100644 --- a/custom_components/panda_status/switch.py +++ b/custom_components/panda_status/switch.py @@ -45,16 +45,6 @@ async def async_setup_entry( device_class=SwitchDeviceClass.SWITCH, ), ), - PandaStatusRGBIdleSwitch( - coordinator=coordinator, - entity_description=SwitchEntityDescription( - key="rgb_idle_light", - name="RGB Idle Light", - icon="mdi:lightbulb", - entity_category=EntityCategory.CONFIG, - device_class=SwitchDeviceClass.SWITCH, - ), - ), ] ) @@ -105,53 +95,3 @@ async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 ) await self.coordinator.async_request_refresh() - -class PandaStatusRGBIdleSwitch(PandaStatusEntity, SwitchEntity): - """Representation of the RGB Idle Light Switch.""" - - def __init__( - self, - coordinator: PandaStatusDataUpdateCoordinator, - entity_description: SwitchEntityDescription, - ) -> None: - """ - Initialize the RGB Idle Light Switch entity. - - Args: - coordinator: The data update coordinator for panda_status. - entity_description: Description of the switch entity. - - """ - super().__init__(coordinator, entity_description) - self.entity_description = entity_description - self._attr_is_on = self._get_state_from_data() - - def _get_state_from_data(self) -> bool | None: - """Get the current state from coordinator data.""" - last_msg = self.coordinator.data - if last_msg and "settings" in last_msg: - list2 = last_msg["settings"].get("list2", []) - if len(list2) > 1: - return list2[1].get("brightness", 0) > 0 - return False - return None - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._attr_is_on = self._get_state_from_data() - self.async_write_ha_state() - - async def async_turn_on(self, **kwargs: Any) -> None: # noqa: ARG002 - """Turn on the RGB Idle Light.""" - await self.coordinator.config_entry.runtime_data.client.async_send( - '{"settings":{"rgb_info_brightness":"100"}}' - ) - await self.coordinator.async_request_refresh() - - async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 - """Turn off the RGB Idle Light.""" - await self.coordinator.config_entry.runtime_data.client.async_send( - '{"settings":{"rgb_info_brightness":"0"}}' - ) - await self.coordinator.async_request_refresh() diff --git a/custom_components/panda_status/websocket.py b/custom_components/panda_status/websocket.py index 292183e..88ca025 100644 --- a/custom_components/panda_status/websocket.py +++ b/custom_components/panda_status/websocket.py @@ -62,7 +62,12 @@ async def async_get_data(self) -> dict: """ try: - async with asyncio.timeout(1): + # 1 second was workable when this connection only ever happened + # right after a user action; now that the coordinator polls + # periodically, that budget is regularly too tight for the + # device's own connect+respond time and was causing frequent + # false-negative "unavailable" flaps. + async with asyncio.timeout(5): async with self._session as websocket: data = json.loads(await websocket.recv()) except TimeoutError as e: @@ -93,7 +98,7 @@ async def async_send(self, payload: str) -> None: """ try: _LOGGER.debug("Sending payload: %s", payload) - async with asyncio.timeout(1): + async with asyncio.timeout(5): async with self._session as websocket: await websocket.send(payload) _LOGGER.debug("Payload sent: %s", payload) From b9cb0b072c2d88dc255679cc7b84f9a028a17ad6 Mon Sep 17 00:00:00 2001 From: David Venter <146400850+BambamNZ@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:25:11 +1200 Subject: [PATCH 02/16] Update README.md Updated contents to reflect recent changes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5f35e7..22206f8 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ _Control your BigTreeTech Panda Status via Home Assistant_ - **WiFi AP** - Allows you to enable/disable the AP. - **RGB Idle Light** - Allows you enable/disable the idle light - - It just sets the brightness to 0% or 100%. + - Redesigned from original to be a light entity in HA with expected controls ### Select Entities @@ -75,7 +75,7 @@ After installation, add the integration via Home Assistant UI: ## Support & Issues -For issues or feature requests, open an [issue on GitHub](https://github.com/ping-localhost/panda-status/issues). +For issues or feature requests, open an [issue on GitHub](https://github.com/BambamNZ/panda-status/issues). ## License From 31956ab010c4142fcb2b54dceed1ce70402b6643 Mon Sep 17 00:00:00 2001 From: David Venter <146400850+BambamNZ@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:59:19 +1200 Subject: [PATCH 03/16] Update light.py added trailing newline (182) to light.py for ruff --- custom_components/panda_status/light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/panda_status/light.py b/custom_components/panda_status/light.py index c77d558..39b0e1e 100644 --- a/custom_components/panda_status/light.py +++ b/custom_components/panda_status/light.py @@ -178,4 +178,5 @@ async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 self._attr_is_on = False self.async_write_ha_state() - self.hass.async_create_task(self._async_reconcile_after_delay()) \ No newline at end of file + self.hass.async_create_task(self._async_reconcile_after_delay()) + From 9a59b92f6c9f9d5a3caa8b48695d7698b5d7c19b Mon Sep 17 00:00:00 2001 From: David Venter <146400850+BambamNZ@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:11:26 +1200 Subject: [PATCH 04/16] Update light.py Remove white space from last line --- custom_components/panda_status/light.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/panda_status/light.py b/custom_components/panda_status/light.py index 39b0e1e..da7b5d8 100644 --- a/custom_components/panda_status/light.py +++ b/custom_components/panda_status/light.py @@ -179,4 +179,3 @@ async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 self.async_write_ha_state() self.hass.async_create_task(self._async_reconcile_after_delay()) - From 4f2e3ea9b8103936dcddf85c0a9c1a0123fe4214 Mon Sep 17 00:00:00 2001 From: BambamNZ Date: Fri, 31 Jul 2026 18:29:03 +1200 Subject: [PATCH 05/16] style: auto-format python files with ruff --- custom_components/panda_status/light.py | 4 +--- custom_components/panda_status/switch.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/panda_status/light.py b/custom_components/panda_status/light.py index da7b5d8..44d6873 100644 --- a/custom_components/panda_status/light.py +++ b/custom_components/panda_status/light.py @@ -90,9 +90,7 @@ def _get_on_state(self) -> bool | None: def _get_brightness(self) -> int | None: """Get current brightness, converted from the device's 0-100 scale.""" - device_brightness = tools.extract_value( - self.coordinator.data, "led.brightness" - ) + device_brightness = tools.extract_value(self.coordinator.data, "led.brightness") if device_brightness is None: return None return round(device_brightness / 100 * 255) diff --git a/custom_components/panda_status/switch.py b/custom_components/panda_status/switch.py index 0ac0833..c8e1211 100644 --- a/custom_components/panda_status/switch.py +++ b/custom_components/panda_status/switch.py @@ -94,4 +94,3 @@ async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 '{"ap":{"on":0}}' ) await self.coordinator.async_request_refresh() - From 5af5f4e2fd2aa872d28d13313be746ff660f2471 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:48:45 +0000 Subject: [PATCH 06/16] build(deps): update pip requirement from >=25.2 to >=26.1.2 Updates the requirements on [pip](https://github.com/pypa/pip) to permit the latest version. - [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/pip/compare/25.2...26.1.2) --- updated-dependencies: - dependency-name: pip dependency-version: 26.1.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5d6df45..44fbd22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "homeassistant==2026.2.3", "mutagen", "numpy", - "pip>=25.2", + "pip>=26.1.2", "pymicro-vad", "pyspeex-noise", "PyTurboJPEG", From 01ce82f6ce8ba629dbe50e8298974a493d88ed7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:56:47 +0000 Subject: [PATCH 07/16] build(deps): bump home-assistant-intents from 2026.6.1 to 2026.6.24 Bumps [home-assistant-intents](https://github.com/OHF-Voice/intents) from 2026.6.1 to 2026.6.24. - [Release notes](https://github.com/OHF-Voice/intents/releases) - [Commits](https://github.com/OHF-Voice/intents/compare/2026.6.1...2026.6.24) --- updated-dependencies: - dependency-name: home-assistant-intents dependency-version: 2026.6.24 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5d6df45..8c7cc8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ dependencies = [ "ffmpeg", "ha-ffmpeg", "hassil", - "home-assistant-intents==2026.6.1", + "home-assistant-intents==2026.6.24", "homeassistant==2026.2.3", "mutagen", "numpy", From 249aeb0f754943c2d53b1d7a623c71a892a14aad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:00:46 +0000 Subject: [PATCH 08/16] build(deps): bump home-assistant/actions/hassfest Bumps [home-assistant/actions/hassfest](https://github.com/home-assistant/actions) from d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d to ab22029681aa532bfe7de5774a9972d67bfbd2c0. - [Release notes](https://github.com/home-assistant/actions/releases) - [Commits](https://github.com/home-assistant/actions/compare/d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d...ab22029681aa532bfe7de5774a9972d67bfbd2c0) --- updated-dependencies: - dependency-name: home-assistant/actions/hassfest dependency-version: ab22029681aa532bfe7de5774a9972d67bfbd2c0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 521ff73..d35f201 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run hassfest validation - uses: home-assistant/actions/hassfest@d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d # master + uses: home-assistant/actions/hassfest@ab22029681aa532bfe7de5774a9972d67bfbd2c0 # master hacs: # https://github.com/hacs/action name: HACS validation From 69ccf8b1600ff38e08b4018c1b9045658b9190e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:02:09 +0000 Subject: [PATCH 09/16] build(deps): bump actions/setup-python from 6.2.0 to 7.0.0 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 7.0.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/a309ff8b426b58ec0e2a45f0f869d46889d02405...5fda3b95a4ea91299a34e894583c3862153e4b97) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d99913a..d0ff9f4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" From a958be169ca48104ae15130043f4e9e4b8d3e5f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:05:44 +0000 Subject: [PATCH 10/16] build(deps): bump actions/checkout from 6.0.2 to 7.0.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...3d3c42e5aac5ba805825da76410c181273ba90b1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lint.yml | 2 +- .github/workflows/validate.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d99913a..a7e11b0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: Checkout the repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 521ff73..1d4667c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Run hassfest validation uses: home-assistant/actions/hassfest@d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d # master From cf448cebdab94119cdad29db6868c13fd63b3e06 Mon Sep 17 00:00:00 2001 From: David Venter <146400850+BambamNZ@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:11:07 +1200 Subject: [PATCH 11/16] Update .ruff.toml Added CPY001 to ignore list - for now --- .ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/.ruff.toml b/.ruff.toml index 73a4fed..574f232 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -15,6 +15,7 @@ ignore = [ "EXE002", # incompatible with Windows "COM812", # incompatible with formatter "ISC001", # incompatible with formatter + "CPY001", # copyright comment block ] [lint.flake8-pytest-style] From eec63b33e2b01f988cdc255a1f856f98ff50fc89 Mon Sep 17 00:00:00 2001 From: David Venter <146400850+BambamNZ@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:18:21 +1200 Subject: [PATCH 12/16] Update dependabot.yml --- .github/dependabot.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4f1901a..79292a1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,17 +4,17 @@ updates: - package-ecosystem: "devcontainers" directory: "/" schedule: - interval: "daily" + interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" - package-ecosystem: "pip" directory: "/" schedule: - interval: "daily" + interval: "weekly" ignore: # Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json - - dependency-name: "homeassistant" \ No newline at end of file + - dependency-name: "homeassistant" From 8ad4745746f61dd32f8584f16b77f18c0a7e1d3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:20:39 +0000 Subject: [PATCH 13/16] build(deps): update ruff requirement from >=0.12.12 to >=0.16.0 Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.12...0.16.0) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.16.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7b8d85e..c1706af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,6 @@ dependencies = [ "pymicro-vad", "pyspeex-noise", "PyTurboJPEG", - "ruff>=0.12.12", + "ruff>=0.16.0", "websockets", ] From c45252b441663da48e4f40936794fb4d1386c532 Mon Sep 17 00:00:00 2001 From: David Venter <146400850+BambamNZ@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:22:40 +1200 Subject: [PATCH 14/16] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 22206f8..1ee0037 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

-Panda Status +Panda Status

Panda Status

@@ -7,7 +7,7 @@ _Control your BigTreeTech Panda Status via Home Assistant_ [![HACS](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/ping-localhost/panda-status?style=for-the-badge) -![GitHub Release Date](https://img.shields.io/github/release-date/ping-localhost/panda-status?style=for-the-badge) +![GitHub Release Date](https://img.shields.io/github/release-date/BambamNZ/panda-status?style=for-the-badge) --- @@ -23,8 +23,8 @@ _Control your BigTreeTech Panda Status via Home Assistant_ - - + +
lint buildMS Buildlint buildMS Build
From dc48225bc0fcd0901664dd6a26b964136fc564ad Mon Sep 17 00:00:00 2001 From: David Venter <146400850+BambamNZ@users.noreply.github.com> Date: Sat, 1 Aug 2026 07:45:21 +1200 Subject: [PATCH 15/16] Update README.md Expanded readme.md with details and updated picture of Panda Aura A1 / A1 Mini --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1ee0037..8e4a8e6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@

-Panda Status +Panda_Aura_A1-6 +

Panda Status

@@ -13,9 +14,9 @@ _Control your BigTreeTech Panda Status via Home Assistant_ ## Overview -**Panda Status** is a Home Assistant custom integration for monitoring and controlling your BigTreeTech Panda Status device. It connects via WebSocket, parses messages, and exposes device data and controls to Home Assistant. +**Panda Status** custom Home Assistant integration for monitoring and controlling your BigTreeTech Panda Status device. Connection via WebSocket, parses messages, and exposes device data and controls to Home Assistant. -**Tested with**: V1.0.1 +**Tested with**: V1.0.0 Panda Aura A1 / A1 Mini --- @@ -47,8 +48,12 @@ _Control your BigTreeTech Panda Status via Home Assistant_ ### Switches - **WiFi AP** - Allows you to enable/disable the AP. -- **RGB Idle Light** - Allows you enable/disable the idle light - - Redesigned from original to be a light entity in HA with expected controls + +### Light + +- **RGB Idle Light** - Allows control the idle light + - **Changes from original integration:** + - Idle light as an entity in HA, with expected light controls ### Select Entities @@ -62,7 +67,6 @@ _Control your BigTreeTech Panda Status via Home Assistant_ 2. [Add this repo to your HACS custom repositories](https://hacs.xyz/docs/faq/custom_repositories). 3. Search for `Panda Status` and install. 4. Restart Home Assistant. -5. Set up via the configuration flow (YAML is not supported). ## Configuration From beb2ecc1ae6685dac3245a0770a17f75158a7cb1 Mon Sep 17 00:00:00 2001 From: David Venter <146400850+BambamNZ@users.noreply.github.com> Date: Sat, 1 Aug 2026 21:15:30 +1200 Subject: [PATCH 16/16] Create icon.png feat(branding): add icon and logo assets for HA integration page --- custom_components/panda_status/brand/icon.png | Bin 0 -> 11330 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 custom_components/panda_status/brand/icon.png diff --git a/custom_components/panda_status/brand/icon.png b/custom_components/panda_status/brand/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d0434321d3c2f65dd5ce92e7e8fe232cd5c78bc7 GIT binary patch literal 11330 zcmdUV_g_;%({B<&3DSx58bCTEAT6{YRVjj?pwgR&l+b(cy;uP0RY0W)h}6(Qks?h& zq)YEeC)~q(pU?Z=`ybrXJOF{fz%3X= zLInI7yO!Esx7;46-vLz&vaA6O=rbj4B@n1O>BfZxA<%|lH6FWxKop(VKX9*esWtFH z)N=zPcOz{rw3V}?h=sMYrHzQUBNpfl0?8?QV=b&+*tm08+B|#iB+t9k)W*y4+*+R3 zP(mB2ja9a>d#>T@YNPL~V_@a`!b--PS5bkSRL&a>P;j(yx8U%0bZ~M*d&?vKi!d6v zzkV!=P>^%AwnaZsQTtC1pe2v6b9cw0MMb^5yhOajMVwuqiK1j=WJHl-qGDpgKo4Oz zA18MUZ(%1luKy5Fv2n9IP0+&9*~48P;o< zLRnh~TUd*ugssKJt)(PT(qgtKOT>SMcYkjCKjS;O{YMOdKt!+Kh@wQ0*DPP(qwl*u zw*k1g9#R1%_h0Y-uV-?i*Nprhj4Ax@C%{~RyZ@HVwV3`}x;9RLNWB2r3B5|c0RjoA z+*MIB@HXAb_VI>4oQMkgTz)**iYye5ZJ0lk5l0sm5D0)F7UU!iVax&?(c*=P;sOPH z9HgP~4xzC(&J%?eA+Zf1H%bfhNTNAl;MBt>CLV2yaDb`86FC2Wy;x+qRa`FYadg8CCe>1w-4Ner(TjTx ztW%=h{uEDeI2Y>o!HKLtPTOfbe~3mZ8&H4Rr{zrvU{| zvl}Orxk(mQ{k%`AAs&wcCY!wIWijGrB7P5|ElHC8a=9ZEW}^FJ-P*u_sC~6uP9Vz- zPJg~P`%hL^!H(ZPG_}58RBXBWsNv)B=}$B2;T%LF?VyZn&F@l}%1FQ*jl+pUQhkig zb1^W7NQe(@V_wliw^U3lc2Li_=w9AR9#3gSf_)|nBaxO*v%J$*fP#P^BrjO71v{9+ znueK_rMI{&$*t8Ey8E&i^skIUMNh#F_QyYnx0ryxgF#u<2E>yl(F?GcvUT6lJehz2 zvxa%u&Y-OA2>qo%Pp4fIh#oGY#)Pu8-8kTNFLuwP=H?1H_RRC&r5S@hh=Ve$BSJ3W zu%c{hZmRf#X&leb-HbXeun80d`m*PqCw!N~W()<+eM>=R8oLhX0fF@ILbTO7-OW*d zK4Z~&nII5{)VG8P+NJ{G1XvN-3dSXc{ly=B9$mk9f-vro!&C-_59Ie&ubI9YdrLF? z5Clrzx<5dX41K^OBfI;D;*g<%ds!oRoSA_eR+LM$hBKLX6}%0>_?M7*q`eGZvw)lrxFk_k34aBZjep z@F$nj8hr{R_N938)-@Zn@QFamQ<47lIRx%eoN@MvK%OW=krf#M0^t@P@eCKvFqY=s z5a&tlzO%D~@!=vOV$>%l@=L2z;VC9~{!S?yXA-9^FKfGpvfy0+!`Y%15`Mi>4;VE2v@=w!cf*6CC@M;~pT5 z(}bS}+yeX!_&>|(=RKeK?Ee~Gov2Ki$LHUoGQ(KcTtc_X?qivX2uQ!u)JwU{**Go;5pfW(27;K_Lt(URb$oZ|xf6Eh~fSad6x*700 z@hSEugC*;9aB0SvSL^V6R&V!J8*jVeV(BVHlE)**3VkwBo&2F20ZMjA0}yEL7|j`$ zKP3N6{1F6V8PT+Wa9UTt-zKJ=_D=yV`@x)P(vHu{Yq-=x}mm9!VFGf}a zh3!zrpe#4<=5?;>GutXC3!*cgS99cJXGSSfh^F)Whio=!=|PLgV%yn((izxqsm|~ zV|jPRHGW&KtBX+;V86p8J^_7`~X;Yz5pCu;=*3y~~s1`4Xn+wb<9ju@%?rlEIY5wH9erNoE%9zUdte0aV6r5r+Wk|KX-~HB$wGXEZwCWaVjuP_Yt?o6In`lK?rj+< zcX{RlK9=bYu9sH6V2rrx&*PkBqZA7oZ~q^}4LfCCoYLuy`?~531PDFGb`11oRGRs4 zLQpv0`Z?P`;)l{8-pdVWCYvS+hzVx>0KI!sM7dk(vXNpo7p4z)|1PnCZVNBtn#vEV zJgC)g_{$Ln}m(+Tu@|7q4i|jLUl16W7IsWlP}S2tkthT zRG(ce&8v2*ZobhbrpZ>U#F)gETg@7};Z=TK@p%NwTFC$`BY#eZuHpKw{G)>s{LIMD zJ znlRYc0}`uBM8qB>LVe`N8LQ?VD6yHlZwH$>h2hpA!M9jf-G*F+f_k&AB@=Zrd2(Xc zW?>No*JpomKRZ{$(&t)HIG68L%SN_hFmCnpgT=zjDI#r+nP4-xKAU4y;PQ%nCRG_O zdB3OdQOB^h5;*VQKeyezzb$?DI&ELagWGk<3UEkE4MAhqc33oj&#OApc%?Toe=E=E zXl(;ZwiN_dH>SI?6{(`j2?7)PPH#qVG3h6wH;b_pl~1qMRplSIt?|}2*(fx#sXuwo zP5DTDcj~9k>i9g_7XM5iADAO>M4gAQP6@`AW|#iyaf{BYw5>?op)gk&^RB*ZP+Lwy z(*b2}1GrKcPul8`bYwtKkQz@LcbR^u@iZ&YHRs4(Ex8{ueEg<}Ad5gJD)@p$>bD3r z+hoa5)nZl*FC10Q?U7Q>l6P^b);9*_>JD9nQ^0oKa{K98xa5V zPVm-ePA#1tSe`;Cjsv+!!Ie|4Pk*GDzHhhKfj?_7L8K4C+KwkFb9+Na+cu6$= zt|pldk}JA=lmQ+GQ`x?2m%AdlEIETm6e5S>9GfWe&v85_F3un8VHOWGK#X@fNARgu ztq1Iaq=r0IJ{y6r$c>^+xWSqjNFRvCB8ZQ}V%_R&=Clq7-d^o=|NZ!SRnDB#bGvLr zl2eNahFch;Jbx$mBs6fj#~lwXhx-3)uA?#8HVU6ne5S#jobLhR2HhxR-I7`#tSoMY zqGwvEnvNcDn?KS3(G>>{&i+KO%g<#MO3y|KKRqryqE=+H0;>aC{^uF@-f zea8#ati&TeID~&&93o^<>CH_I6d}>(?!=PySOqQ~oF7dlBG_>JHx9c-$Xm+n%CyT& z3ITRZqy52SZ;NfZr4>JpRewSlq_Z{m#YoHLnh~=4X`#qmds0ipis>aJ!l$iO5Nicw z0eLgsycWO4T(|i3Ke^o}7im3v$nqfpF}dWWSQ6lmc(QT@IJm92%2E z#rx$N$Gv+O5FcvUwoOA4&QmMLPsu*iR-yOMn>#r2gYjq9mMD=ZGTe>_f_0{c45D!v zi$ujFpbGLCOm9D4Ar7uS{@h+QjgK#n<#@H&=EOuyY&;l$l1^S+>v3a%b&?Sw4dLuh zX3fond^2P0zuXO_DUq9?XwjNDrHyZ?B#)f&q%IeLJpbZHbvBy+k3U_$LY#$#B)lNu zUfDowF`;hT!To>_tWN7AC&cEf{XfsA=A1uYF`f%TbTuGvimW#1(hnS-N#m$Ux`o(b zMvp*`HZEX27lj^jta%!^a`%&sYm#IXxc#+Y9>U?ggQO~*mHT-m1aO#9#*YbMq@OXP z3il82i+ea%i22@kq_UES_&}!6S;I#4ab0`M=gaQ&5G920t9qx9FN=#bBv14qZ)h+J zgX|Rd(vOV%z5(`fe`fM65s|P?R9ihR%O!i>d_s#@h%2~JwvFj%)~>qhDota6VzaWy z9amnuhItm9_0-7|M+4Dt$74Bi4xU~NR@!U^IPM-#i>mRl6l&lK{%f!y&zK~b#+#Jj zH9_zO<%zc62YIJWm&E$JJ=nXsle^tguXoS0ru2KYIJoC8MoFvi|6T|g!ur4;W^0*8 zO39L@Q|f7c_fWmOnYG`DAFP z9#bZhSPkw--L701wA)MmI@;xBEN~zdc>h>t>xd$>Lf7qo3!mZ z)~VJ{*tn><`-a(-BHGe7=t?2`iRDp)7?MJTN7b+KuHGovBerUmK#*DD7V`MlAl8M0 zYAk4S^^D;OjDfVb>U8?v#qw3)dwCBP$$(Swqs@;I$$#VcOpqb%J6<8EU{vJWwR6j0L=m{lDNJSTgb4)!>cBqDQC(?D(T4>!#uQhi*Xd0wf{{^S|h& z=64DwLqgvBIc1Vpn8p|AYrV<)iejvREd6cZWO?@3-pV4L6P^MHVSsg((WC;cJ-uZ3 zoD>WS7IIrU$;LLZv>73$jTo%3O{$w8$?oViRFq*t!=W7(F!5X=?Jo{L?SwdkUBo3V zf4+Q?lD!x*x+MF1;>u0TcA&F}f#8k>}p71KpU(TfbQ_A{RS=io>?YO(zXYJM@0(6G{9*Z2J&Zi@!DFq!a}GNgK` zy|yi$qBL7E(g>idmd2*P;%slMqxbQHTk#q;F48NoYk9{r4(rFVdNnkj{(1qImy-Sj zv}o(lHOxDKkhhZfD6f0sElk6FbbOB^bOXE@knv z^m^!yQ2+qd3hbp*DrgV&%)CpEd1p#$7qFMMGt**O=fDU+&2TgEk1 zL#R?Igvy0#@c^!DWvNn;NP1ocf-OX*9faVHs> z*!z~aB=nWu-N?(-GmOC}1=^I-IOuOvkDK%FzYWmBfMgGtc+uc234;}hx@9t=hUv)b zUeM3`>`K0O$;f==D1N(N-5a=9?a)vLB%X^j2Nyn0r04n^)JK|ns>1uv3#!awzs}WI zYCJS40g$Xi->imZ{CJ55G1*PPhvcez;a?3$UTQyH8a<8G=OkTMAO|3?YfCe&GwNIY z1@46AQJj1%9LzZLX&F4codVTd*98}S%&>vc^vBRScGm)bS+YpUc$A6a|4Ca!0 z$OMszCfYYZqt~945jTuX{;a54B-n70>i}b_fOW}mS=bT6X(I~sr1+QSojMw0dW)` z$?kdflztltK__JsrNw%)Fp0YLNq|lm_wel$%oD=o^c5+@rlIRJMSL#&0H10Lw;_L^|+q$-5B~p=jH$0o!45q!j%J>u8UQ1&Bd*FmEkSP90yhOna34evOvL@Cf%b}hq zvby*ZNQTk%f{lHrI1Dm>I3;;g#tF>b8A#x{V?(xX)lEQCl4uw8LTLXsT}R`qy5}J> zi(heoFAAj7<(bpv3OP3{wowdOq`_Q9d9JqjRZg7(IG3nWTTaHgwMB=qyZsELQc<4> zj6s&}&<1YoNM#w8rI=?+Mu%$ z)u+L5@XarG&*qoZGf})N{nkiQfgOvqoHI7DT!0S$O?`R2!rnRFqbiy296C0i`YoIc zCoITW1A%TKRk%7(L4hQ^xXr97OmK){~ytZY@+SvV%bFC;! z*g2r;Ms83G<*Cn~rJZjwd0p0rt5_n^gXXEU^qhapUW-kv#L3xPA^SIS^WStmV~XPk z0XT5G>dF;SkaSZ2ufroSA z`gc!F(*GIo9`ebUxc$g-ICEuh{L>0*YI_e6Eah#<3!g}n>fPMS-kV0W9odxFUiQqa zx@_DrFqpVAu-6sD`*5s{Xr+0ZD}f)#9K3BDc-_V4pD~i$We{6%)XKe}*4-qgHE`i7 z!EJcVjaS>O{t>A=W=-r{m33#NtDGWjP(ha9zj$LpSK&y%JBEjw-X%vi+L8GXP_VKD z^&v|w)H$%~Mt650mZ$DB7cvr~5z3;!`}L;|-~BxT9e7DxNex<;FZ2Rq)kmpn+) zrSb(UnwPk@zP*Ka_JUkL%^2+(OBs9g_m}=Oybqe>l3Sl8d3bY^6KsaOdR12>J6pekq{rNEDGtSvn{9SSnQe}@b{%H9`EtI|-VS*g zds&KRc(7x)m$wy7Obl1O%%V;r0-#)Y5B3_O!yg?_v0ISd!k-I}t$T4E84{)L%x+Lq z$MjTT|2=X%=scM{Ca&83zLRXWs&Bxkh(6yBmDT%b=JS`6>xpL^?PXda@V>|3Pas^J}=ZPnMB%8S|9!hoF@x zM&L|@_rYbh^ud^a*5R~+f3uJ19co6I$0E@XWe>;O8G>SIwo01_AsjBWa&U&x8dxI-8{>#Hw(6j zHM$$#T)Qf$mD$7?L1>EX!7*SYVL5_$0-pI7@)# z5a{n4`e)v`5#SOEBUV+OXTU$elh$7u6I4#S7(>zzcON!<+98}OX|#xi=T+gNL|cEx z)lAdn6$li`2Qb8gY1!=KX0Ie~q(n1`xed-A%TUL5Asv=vOVRL&)`41wt39f1<~q6{ z<`%juh69mpHvFS|GBzd1z4`jppMQbE`&h(y0gi)(KO6~;sV;K|it$@%?%i<6Tvpi4 zvugY&`Bj%mO9QqO4;WB`H??eL{+a<;_q17~!`1413jj3v0vKwoQ7IuVBr>s7*X-3|g>@CKO5^Cgl0} z_C;G0|F17iOoqm?%(L6*=!MzkyoTYp1&C-kLXnj8BPUY$=0nZ_qg|diZ3h8`+o>s~ z{#_`7g6F6t&_W8vB7`zGKtE(V>`mT5uRPcGSS5zQ=8u8ZnAZ=PN@&c6P@6n~GD)b+QKObd&lQRx8z zrhYc!IoI^!gy~9tW4(pC-UFd%F=jwLf6sMwmwIa?o}#cPnF+B_k^odTh=#l5IQ1&= zs!j6VQkF729o=TH$HwH(UvnwaWo<fV>L@7Jo589I=w*JGS|p^o2fj2jyXL%W$y*`dyVWvx`sh{47mJ zk6vW5stM%~jYtQ#rr!gF;&gqG;iLBly9ZLXP*=Z1(#0!;e6Ca6O2rwNqo@Mx=4)`> z(kp_;3iL-?lnWK}&QEGmb%X`S10`tZu(6ODfAz{OyMU3Mko0&vEl${K^m8aK{RxB1 zQ5m6;Tp6>|swYLGsgHo~tE?Qk#?SKP^3%CMgoiF{UUG-7zlNR%=!_BZbgn#%L3>}g z{cA`x{pGFwSX1tD?saKJA_vp!m(-@WQeHlk^0cAT;sjz8KH^a6m0HM$^xp*E?+=Vf zJ`Wz+z`a=fiv8jIi8`^|%m49w-%*p0iCX4!B#6c1Tc8)-AfX*_Czn>Uonea;yh&{d zrctNwKQH9q=@%3`rxC`qN(MFf+PSlYroF?lBvb&NPAm5_;pj>bY)}TOe~7-R-~76~ zoa60hK3w_pW$Du{hpn~FITi-#3+>H6^ug0*o+n-Vb9S!~pijxGC#@$X^t`$01WB4* zrF$Ew{ZmSm>%KaJ%qdrV_7TjN{7EQ~H7^|fxr%xgm6lg>3v#!n=UKnZoIA^(*RcmJ z=@)184S+>8Y~@QZ9CYwvWx9z*68M;2eCeF617EEDo;Ck2*y3`hnw{Oho|rwLnx5TJ z;o@h*^0N=KhqO)?z0B#7&Nt-PqR`^FvDAAWQH&cdMZd`3BS`irx!LgIpsSfGg4_U; zko)x)@lPuh*iD+U$dh_Q3j9T5_gnZxjyOeU5pdtLmJxBe~gY}xL`N;D}k_|t@Ebf}@^^MEs%x=gze7MEA7e?IaeTJ88cy|>+ zKA0J!Tt|KJ<>KaM8EHH1?6`U=yvw?{LV-spf1Hrle67~Ck5qk4_{Q%1iXW4MFKuEM z)-r`%@LgJA_l_Ggo+=xu3f0xSc_%7!#cHN!W_P7=-ZMsB&RB<9A*s+}B}tSZ=bwSP zre`21*(UKV2EKmzpqu51lN>5TdF0hU-x-~Tto3KBR1sW*W8599&2pmK0j|P1f%CKg zP=i|_wDVwo3o207z$XG{C^n5nC!D4dw!K6mhZb?`^u7zC^jX2Nu^2JJU7xIUs!5XNz_pK`K@D%q~2i~E``(Z~fre`S{w zZ}Ne;3@?_CV$Fk)r{|2>%xr?iK)A?0^gQxwxvq&GmA*c~*0WO8*C1u59OC1|CsEIA z9e@e}q0D0^WUpnO}P5S{m?2fxtt@@7L8 z!d7ksJn1R_)NjHS$TKRK>sbyAoeABduidfL$(&~0ccJzQd5KjSJD39gkQW!QgqDX!b-01x_)bET=|jq%=Nr+mE2K?Shfbw@5gWvZe$?HOpu>^45_= zeYwzSV8J!!uys(LJcX<4HA{w~e$ALssC-?ZA_Yqs_zHN9_7MBWJQ zGUp^He}Bi$&Bv|QahwH+M);VkNZ12fiND8Q0D&n7a#Jzyo_Y6Sj$KZNgAp1yw&W)W zS#>*p@WJogPnsqS5wAqitNVK(DT20fu10OUE{7jDcZB|z8znx(*HNTCWq<$K?JJgZ zk(~5H&h%a|cwOj@R%*x;iJIbXI^{Dn5<~x~$@R_mqy^{Man=pCJU!N%~0d p_Yo>l)d&ASly&}32K@I5EWH0%$r+*Ee|?bmuBwho#U1m|{{`+^q-+2H literal 0 HcmV?d00001