Skip to content

Typed - #160

Open
markus-becker-tridonic-com wants to merge 21 commits into
sde1000:masterfrom
tridonic-com:typed
Open

Typed#160
markus-becker-tridonic-com wants to merge 21 commits into
sde1000:masterfrom
tridonic-com:typed

Conversation

@markus-becker-tridonic-com

@markus-becker-tridonic-com markus-becker-tridonic-com commented Oct 5, 2026 •

Copy link
Copy Markdown

Summary

Make python-dali a typed (PEP 561) package so downstream consumers' type
checkers follow it as a typed dependency. Ships py.typed markers for every
public subpackage and annotates the public signatures consumers call.

What's included (top commit)

  • py.typed markers for dali, dali/device, dali/driver, dali/gear,
    dali/memory, included in the built distribution via MANIFEST.in +
    include_package_data.
  • Public-signature annotations: Response/Command (command.py), the
    address constructors, the gear/device command bases, the commissioning and
    query sequences, memory-bank read_all, and the typed exceptions.
  • from __future__ import annotations where PEP 604 unions require it on the
    3.7 floor; fixes the malformed dict[(int, int), int] →
    dict[tuple[int, int], int] annotation.
  • Signatures only — no behavior change.

⚠️ Stacking

This branch is stacked on the full integration feature set (serialx
migration, part 306, DT8 colour sequences, control-device commissioning, DT1).
The typing change is the top commit (574b461c13cb3606e14aa585f9d4bbf27805ede2); against master the PR
shows ~20 commits because the annotations cover symbols introduced across those
branches and cannot stand alone. Review the top commit, or retarget the PR base
to the appropriate branch once the underlying stack has landed.

Testing

  • python-dali suite: 161 passed.
  • Downstream verification: the Home Assistant DALI integration passes strict
    mypy with 0 errors against a non-editable wheel of this branch, and
    py.typed is present in the wheel RECORD.

pyserial-asyncio is deprecated. Switch the RS232 serial drivers to
serialx, which provides a compatible create_serial_connection and
SerialTransport API, and update the driver-serial and test extras.
Bump version to 0.12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build sdist and wheel and publish to PyPI via trusted publishing when a
v* tag is pushed. Verify the tag matches the package version before
building.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
serialx's SerialTransport has no `loop` attribute, so the LUBA and SCI
connection_lost handlers raised AttributeError on disconnect. Calling
loop.stop() was also a leftover from the standalone-script era and would
stop the shared asyncio event loop of any embedding application.

Signal the disconnect through the existing connected event instead, which
mirrors connection_made setting it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lunatone DALI interfaces with an integrated power supply (e.g. the DALI-2
USB PS) leave the bus unpowered by default, so every transmission fails
with "error in transmission: 1".

Add an opt-in `bus_power` option to DriverLubaRs232 that sets HardwareSettings
bit 7 when writing the device settings, switching on the interface's bus
power supply.

To let callers decide safely (a separate DALI power supply must not clash
with the interface's own), implement the two queries needed to detect the
situation:

- QUERY DEVICE DESCRIPTOR (0x28/0x29): exposes whether the interface has a
  switchable bus power supply (HardwareFeature bit 7).
- READ STATUS (0x2C/0x2D): exposes whether the bus currently has a voltage
  error, i.e. is unpowered (Status bit 7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the LUBA device reports an "ADD DALI FRAME TO TX BUFFER" error, no
frame reaches the bus, so the "frame sent" event that normally confirms a
transmission never arrives. send_dali_command() therefore blocked until
the confirmation timeout before failing with a generic error.

Surface the rejection immediately: enqueue a transmission confirmation
carrying the interface's reason code, and raise a new TransmissionError
from send_dali_command() as soon as it is dequeued. The reason codes are
enumerated in LubaTxError (LUBA documentation section 2.1.1), letting
callers act on specific causes such as an unpowered bus (bus voltage
error).

Also log frames that cannot be decoded from their transmit echo at debug,
including the raw bytes, to aid diagnosis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The LUBA device echoes each transmitted frame back so the driver can
verify it against what it sent. Some valid frames (for example extended
DT8 colour queries) cannot yet be round-tripped through
Command.from_frame, but the frame was still sent successfully, as the
interface itself confirmed. Emitting a warning for every such frame floods
the log during normal polling for a purely cosmetic decode gap, so drop it
to debug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DALI-2 USB PS (article nr. 24138246) has been exercised successfully
with this driver, so add it to the set of tested article numbers alongside
24166096. Collect them in TESTED_ARTICLE_NUMBERS and widen the "untested
device" warning to list every tested number.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add sequence helpers that wrap the application-extended queries so callers
get the required EnableDeviceType prefix automatically:

- QueryDT8ColourTypeFeatures and QueryDT8ColourStatus (DT8 / 62386-209)
  report which colour types a gear supports and which one is active.
- QueryDT6DimmingCurve (DT6 / 62386-207) reports the configured dimming
  curve so a linear curve can be detected.

Sent bare, these commands time out because the gear ignores an
application-extended command without a preceding EnableDeviceType.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A query that received no reply defaulted to a bare Response(None). That
response passes check_bad_rsp while its value is None, so a timed-out
QueryInstanceType leaked None into control-device autodiscovery and
crashed on int(None), aborting driver setup.

Default a query to the command's own response type instead, so a timeout
is reported as a missing value and check_bad_rsp rejects it as expected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Before each transaction the drivers clear any DALI frame left buffered
from a previous exchange. A backward frame that arrives after its query's
receive timeout is such a leftover and is expected on a busy bus, yet it
was logged at CRITICAL on every occurrence, flooding the log.

Two bugs made this worse: the queue was reported as holding N items but
only a single item was ever discarded, and the SCI info-queue branch
drained the raw-frame queue by mistake, so the info queue was never
cleared. Drain each queue fully through a shared helper and log the
discard at debug level.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MeasurementVariable, UnitOfMeasurement and QuantityName values were
taken from an earlier draft whose numbering differs from the published
FDIS. Several codes were off, so a sensor's reported quantity or unit
decoded to the wrong type. Renumber all three enums to the FDIS tables
(including the newly added quantities and units) and rename the MAX/MIN
input-value variables to MAX/MIN_MEASURED_VALUE to match Table 17.
A spontaneous event from an input device can corrupt the backward frame
of the first QueryDeviceStatus probe during a scan, which made a present
device (and all its instances) look absent and get dropped. Let the bus
settle after entering quiescent mode, and retry a status probe that comes
back as a framing error while still treating a genuinely missing response
as an empty address. Both the settle delay and retry count are tunable.
Control gear had a Commissioning sequence but control devices (part 103)
did not, so unaddressed sensors and pushbuttons could not be assigned
short addresses programmatically. Add a Commissioning generator mirroring
the gear one, using the part 103 24-bit randomise/compare/withdraw search
and INITIALISE semantics (0xff all, 0x7f unaddressed). The bus is held
quiescent for the search, a verify failure is raised only after quiescent
mode is exited, and a same-random-address clash re-randomises and retries.
The fake device bus gains the addressing commands so the sequence is
tested end to end.
The initial device-info handshake can time out while the bus is busy (for
example when an input device is streaming events), which failed the whole
connect. Retry the handshake a few times, draining any late LUBA reply
left in the RX queue between attempts so it is not mistaken for the next
command's response, and only give up after the configured attempt count.
A device reporting more than 32 instances from QueryNumberOfInstances
(a non-conformant device, or a count whose backward frame collided with a
spontaneous event) drove the instance loop past instance number 31 and
raised ValueError from InstanceNumber, aborting the whole scan.

Instance numbers are 0..31 per IEC 62386-103, so a device has at most 32
instances; cap the count defensively. Non-existent instances are already
skipped when they do not answer QueryInstanceEnabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The IEC 62386-306 Annex A, Table A.1 unit-of-measurement list ends at
53 (parts per million). Real hardware (Lunatone sonuSense) reports unit
codes beyond that: 55 for A-weighted sound pressure level and 56 for a
manufacturer-defined probability instance. Decode them instead of
surfacing them as unknown values, clearly marked as non-306 extensions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the IEC 62386-202 self-contained emergency control gear
variable definitions and sequence helpers consumed by application
controllers:

- dali/memory/emergency.py: memory bank 208 (Table 18) with the control
  gear temperatures, battery charge/maintenance powers, rated duration,
  recharge/function-test times, and the full set of failure, mode, test
  and lamp counters.
- dali/gear/sequences.py: QueryEmergencyInformation bundles the DT1
  status and measurement queries into a single sequence (so the required
  ENABLE DEVICE TYPE 1 prefix is injected), and EmergencyCommand issues a
  single DT1 control command.
- Fake gear emergency support plus tests for the bank, the information
  sequence and the control-command helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The driver-serial and test extras now depend on serialx, which requires
Python >= 3.10. Installing the test extra therefore fails on 3.8 and 3.9,
breaking the "Install dependencies" step of the test matrix. Drop those
interpreters from the matrix and raise python_requires to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ship PEP 561 py.typed markers for every public subpackage (dali, device,
driver, gear, memory) and include them in the built distribution via
MANIFEST.in and include_package_data, so downstream consumers' type
checkers follow python-dali as a typed dependency.

Annotate the public signatures the consumers call: Response/Command in
command.py, the address constructors, the gear/device command bases, the
commissioning and query sequences, memory bank read_all, and the typed
exceptions. Signatures only; no behavior change. Add
`from __future__ import annotations` where PEP 604 unions require it on
the 3.7 floor, and fix the malformed dict[(int, int), int] annotation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant