Skip to content

Commit

Permalink
Fix inMix to post all the zone structure data in one go - hope this f…
Browse files Browse the repository at this point in the history
…ixes the inMix driver

Some more debugging
Allow accessors to specifiy that they only update the struct so that we can delay unti we are ready, support for inMix.
  • Loading branch information
gazoodle committed Mar 6, 2025
1 parent f763948 commit 1b4858a
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 4 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ because that was to mitigate the missing change updates. Anyway, quite a big pro
a retry loop resulting in tardy updates in HA.
- Protocol retry count removed because it is now intrinsic to retry operations every so
often until the timeout period is exceeded.
- Noticed that inMix updates seem to always send 7 x the number of zones worth of data
based at 600 (which is the SpaPackStruct.xml's start location of the inMix log structure)
so replicate this behaviour in the hope that it will drive the inMix modules correctly.

## Done/Fixed in 1.0.10
- Handle TempNotValid error
Expand Down
5 changes: 3 additions & 2 deletions src/geckolib/async_spa.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def __init__(
self.version = ""
self.config_number = 0

self.struct: GeckoAsyncStructure = GeckoAsyncStructure(self._async_on_set_value)
self.struct: GeckoAsyncStructure = GeckoAsyncStructure(self.async_on_set_value)
self._last_ping_at: datetime | None = None
self._needs_reload: bool = False

Expand Down Expand Up @@ -618,7 +618,8 @@ async def _async_on_partial_status_update(
for change in handler.changes:
self.struct.replace_status_block_segment(change[0], change[1])

async def _async_on_set_value(self, pos: int, length: int, newvalue: Any) -> None:
async def async_on_set_value(self, pos: int, length: int, newvalue: Any) -> None:
"""Set the data block to a specific value."""
try:
assert self._protocol is not None # noqa: S101
if not self.is_connected:
Expand Down
5 changes: 5 additions & 0 deletions src/geckolib/automation/async_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,8 @@ def devices(self) -> list[str]:
Keys can be passed to get_device to find the specific device.
"""
return [device.key for device in self.all_automation_devices]

@property
def taskmanager(self) -> GeckoAsyncTaskMan:
"""The facade's task manager."""
return self._taskman
48 changes: 48 additions & 0 deletions src/geckolib/automation/inmix.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -46,6 +47,12 @@ def __init__(self, facade: GeckoAsyncFacade, inmix: GeckoInMix, zone: int) -> No
f"InMix-BlueLevel{zone}"
]

# Set accessors for direct struct update
self._mode_accessor.direct_update = True
self._red_accessor.direct_update = True
self._blue_accessor.direct_update = True
self._green_accessor.direct_update = True

self._mode_accessor.watch(self._on_change)
self._red_accessor.watch(self._on_change)
self._green_accessor.watch(self._on_change)
Expand Down Expand Up @@ -196,6 +203,8 @@ def __init__(self, facade: GeckoAsyncFacade) -> None:
if "InMix-PackType" not in facade.spa.accessors:
return

self.struct_begin = facade.spa.struct.inmix_log_class.begin

self.number_of_zones: int = int(
facade.spa.accessors["InMix-NumberOfZones"].value
)
Expand All @@ -216,6 +225,39 @@ def __init__(self, facade: GeckoAsyncFacade) -> None:
# RGB 0-255 per normal
#

self.zone_1.watch(self._on_change)
self.zone_2.watch(self._on_change)
self.zone_3.watch(self._on_change)
self.syncro.watch(self._on_change)

# Start task for
# facade.taskmanager.add_task(self._in
facade.taskmanager.add_task(self._inmix_update(), "inix Update", "FACADE")
self._update_event = asyncio.Event()

async def _inmix_update(self) -> None:
_LOGGER.debug("inMix update task started")
try:
while True:
await self._update_event.wait()

_LOGGER.debug("Perform struct update")
block_size = len(self.zones) * 7
data = self.facade.spa.struct.status_block[
self.struct_begin : self.struct_begin + block_size
]
await self.facade.spa.async_on_set_value(
self.struct_begin, block_size, data
)
self._update_event.clear()

except asyncio.CancelledError:
_LOGGER.debug("Facade update loop cancelled")
raise
except Exception:
_LOGGER.exception("Facade update loop caught execption")
raise

@property
def zones(self) -> list[GeckoInMixZone]:
"""Get the available zones."""
Expand All @@ -224,3 +266,9 @@ def zones(self) -> list[GeckoInMixZone]:
for zone in [self.zone_1, self.zone_2, self.zone_3]
if zone.is_available
]

def _on_change(
self, sender: Any = None, old_value: Any = None, new_value: Any = None
) -> None:
self._update_event.set()
return super()._on_change(sender, old_value, new_value)
12 changes: 10 additions & 2 deletions src/geckolib/driver/accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class GeckoStructAccessor(Observable):
@staticmethod
def pack_data(length: int, data: Any) -> bytes:
"""Pack the data into bytes."""
if isinstance(data, bytes):
return data
if length == 1:
return struct.pack(">B", data)
if length == 2: # noqa: PLR2004
Expand Down Expand Up @@ -55,6 +57,7 @@ def __init__( # noqa: PLR0913
self.bitpos: int | None = bitpos
self.items: list[str] | None = None
self.maxitems: int | None = None
self.direct_update: bool = False

if bitpos is not None:
self.bitmask = 1
Expand Down Expand Up @@ -230,8 +233,13 @@ async def async_set_value(
self.length,
)

# We can't handle this here, we must delegate via the structure
await self.struct.async_set_value(self.pos, self.length, newvalue)
if self.direct_update:
self.struct.replace_status_block_segment(
self.pos, GeckoStructAccessor.pack_data(self.length, newvalue)
)
else:
# We can't handle this here, we must delegate via the structure
await self.struct.async_set_value(self.pos, self.length, newvalue)

def diag_info(self) -> str:
"""Get diagnostic data for this accessor."""
Expand Down
8 changes: 8 additions & 0 deletions src/geckolib/utils/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,8 @@ async def _async_on_pack_command(
await self._async_handle_pack_set_value(
handler.position, handler.length, handler.new_data
)
else:
_LOGGER.error("Unhandled pack command")

async def _async_handle_pack_key_press(self, keycode: int) -> None:
"""Handle a key press command."""
Expand All @@ -780,6 +782,12 @@ async def _async_handle_pack_set_value(
self, pos: int, _length: int, data: bytes
) -> None:
"""Handle set value."""
_LOGGER.debug(
"Replace status block segmemnt @ %d with %s (%d bytes)",
pos,
f"{data}",
len(data),
)
self.structure.replace_status_block_segment(pos, data)
if self._send_structure_change:
await self._structure_change_queue.put(pos)
Expand Down

0 comments on commit 1b4858a

Please sign in to comment.