Metadata-Version: 2.4
Name: py-dsc-it100
Version: 1.1.0
Summary: Async Python driver for the DSC IT-100 serial integration module
Project-URL: Repository, https://github.com/santi-lenovo-mini/dsc_it100
Author-email: Santi Gutierrez <santigutierrez14@icloud.com>
License: MIT
License-File: LICENSE
Keywords: alarm,asyncio,dsc,it100,security,serial
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Home Automation
Requires-Python: >=3.10
Requires-Dist: pyserial>=3.5
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: flake8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# dsc-it100

Async Python driver for the DSC IT-100 serial integration module, with a
typed event model, partition/zone state tracking, and a synchronous client
for threaded hosts.

## Installation

```bash
pip install py-dsc-it100
```

## Quick start (async)

```python
import asyncio
from dsc_it100 import IT100, ZoneEvent, PartitionEvent

def handle_event(event):
    if isinstance(event, ZoneEvent):
        print(f"Zone {event.zone}: {event.kind.value}")
    elif isinstance(event, PartitionEvent):
        print(f"Partition {event.partition}: {event.kind.value}")

async def main():
    panel = IT100('/dev/ttyUSB0', baud=9600)
    panel.on_event(handle_event)
    await panel.connect()
    await panel.request_status()   # populate panel.partitions / panel.zones
    await panel.request_labels()   # PowerSeries only

    print(panel.state.snapshot())

    try:
        await asyncio.Future()     # run until cancelled
    finally:
        await panel.disconnect()

asyncio.run(main())
```

## Quick start (sync)

For synchronous or threaded hosts (plugin systems, scripts, REPLs),
`IT100Client` runs the event loop in a background thread and exposes
blocking methods:

```python
from dsc_it100 import IT100Client

with IT100Client('/dev/ttyUSB0') as panel:
    panel.on_event(print)          # callbacks run on the driver thread
    panel.request_status()
    panel.arm_away(partition=1)
    print(panel.snapshot())
```

## Typed events — the public data contract

Every incoming packet is converted into a frozen dataclass (`dsc_it100.events`).
Consumers should depend on these types and enums instead of raw 3-digit
command codes:

| Event | Key fields | Covers |
|---|---|---|
| `ZoneEvent` | `kind: ZoneEventKind`, `zone`, `partition` | open/restore, alarm, tamper, fault (601-610) |
| `PartitionEvent` | `kind: PartitionEventKind`, `partition`, `arm_mode: ArmMode`, `user`, `code_length` | ready, armed, disarmed, delays, alarm, closings/openings, code required (650-673, 700-751, 840/841, 900) |
| `PanicEvent` | `kind: PanicKind`, `restored` | duress, F/A/P keys, aux input (620-632) |
| `TroubleEvent` | `kind: TroubleKind`, `restored` | battery, AC, bell, TLM, wireless, tamper... (800-843) |
| `LabelEvent` | `kind: LabelKind`, `number`, `text`, `zone`/`partition`/`output` | broadcast labels (570), classified per the developer guide |
| `SystemEvent` | `kind: SystemEventKind`, `data` | ack/errors, time, temperature, version (500-580, 908) |
| `KeypadEvent` | `kind: KeypadEventKind`, `data` | LCD/LED/beep virtual keypad updates (901-907) |
| `GenericEvent` | `data` | anything else |

The raw packet dict is always available on `event.packet`.

`ZoneEvent.partition` is only present for alarm/tamper events — zone
open/restore (609/610) carries no partition on the wire. The state store
learns zone→partition mappings from alarm/tamper events; you can also
provide one up front: `IT100(port, zone_partition_map={1: 1, 5: 2})`.

## Panel state tracking

`panel.state` (a `StateStore`) tracks the last known state of every zone
and partition. `panel.state.snapshot()` returns a JSON-serializable dict —
ideal for handing to another application:

```python
{
    'partitions': {1: {'partition_id': 1, 'state': 'ready', 'is_armed': False,
                       'arm_mode': None, 'trouble': False,
                       'label': 'Main Floor', 'last_user': None}},
    'zones':      {4: {'zone_id': 4, 'partition_id': 1, 'is_open': False,
                       'in_alarm': False, 'tampered': False, 'faulted': False,
                       'label': 'Front Door'}},
}
```

Important IT-100 behaviour (see the developer guide): on power-up the module
only reports partitions it has detected activity on, and assumes defaults
(READY/CLOSED) otherwise. This driver therefore reports partitions as
`unknown` until the panel says otherwise — call `request_status()` after
connecting and treat `unknown` accordingly.

## Command acknowledgement

The developer guide requires waiting for a 500 (ack) / 501 (error) response
before sending the next command. Commands are serialized and, by default,
awaited:

```python
from dsc_it100 import PanelError, CommandTimeout

try:
    await panel.arm_away(partition=2)
except PanelError as exc:       # 502 System Error
    print(exc.code, exc.description)   # e.g. '024' 'Partition is not Ready to Arm'
except CommandTimeout:
    print('IT-100 did not acknowledge')
```

Pass `ack_timeout=None` to restore fire-and-forget behaviour.

## Commands

```python
await panel.poll()
await panel.request_status()
await panel.request_labels()
await panel.arm_away(partition=1)
await panel.arm_stay(partition=1)
await panel.arm_no_entry_delay(partition=1)
await panel.arm_with_code(partition=1, code='1234')
await panel.arm_with_auto_code(partition=1, code='1234', mode='stay')
await panel.disarm(partition=1, code='1234')
await panel.trigger_panic()
await panel.command_output(partition=1, output=2)
await panel.bypass_zone(zone=3, code='1234')
```

Partition arguments are validated (1-8), zones 1-64, outputs 1-4.

Note: `bypass_zone` and `key_press` drive the virtual keypad, which only
operates on the partition the IT-100 is enrolled on.

## Legacy APIs

The packet-dict APIs remain available and unchanged:

* `panel.on('partition_armed', cb)` / `panel.on('650', cb)` — per-command callbacks
* `handler_zone_update` / `handler_partition_update` / `handler_general_update`
  — `(driver, packet)` handlers

## Requirements

- Python 3.10+
- pyserial
