mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-29 19:31:12 +02:00
The CI ran tests only. Added a lint job with two narrow gates: ruff with select = E9,F — syntax errors and pyflakes (undefined names, unused imports, broken f-strings). Style rules are deliberately off: the full default set flagged 202 things, almost all cosmetic, and a gate nobody reads is not a gate. Fixed the 9 findings it did have (8 unused imports, 1 empty f-string). mypy, clean at 13 errors fixed. None of the 13 was a live bug, so every fix is an annotation or a local rename: the two accumulators in advanced, the block map in appprog_parser, a reused loop name in project and in repair, a TypedDict lost through dict(), a mixed-type summary dict in handover. Two are worth naming. _CH_TOKEN_RE now captures the letter prefix directly instead of re-matching its own capture, which removes an unguarded .group() on a possibly-None match; the regex accepts exactly the same strings. repair's status-DPT lookup now spells out the None case, which is what dict.get already did since None is not a key there. Verified the fixes moved nothing: the full suite passes, and the corpus guard reports no drift across all six real projects, the 3646-GA one included. Matrix extended to 3.13 and 3.14; both were smoke-tested locally first. Dependabot config for pip and github-actions, weekly and grouped so a quiet week is one PR. Dependabot security updates and CodeQL default setup enabled in the repository settings.
142 lines
11 KiB
Python
142 lines
11 KiB
Python
"""DPT classification and Home Assistant KNX platform mapping.
|
|
|
|
Central knowledge base translating a KNX Datapoint Type into:
|
|
* a functional *category* (lighting / shutter / hvac / sensor / scene / energy / diagnostics)
|
|
* a *kind* (command vs status vs sensor)
|
|
* a suggested Home Assistant KNX platform + value_type
|
|
|
|
Everything here is heuristic but conservative: when in doubt we mark the
|
|
datapoint for manual review rather than guessing an entity wrong.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional, TypedDict
|
|
|
|
|
|
class DptInfo(TypedDict):
|
|
category: str # lighting|shutter|hvac|sensor|scene|energy|diagnostics|unknown
|
|
kind: str # command|status|sensor|unknown
|
|
ha_platform: str # switch|light|cover|sensor|binary_sensor|climate|number|scene|button|unknown
|
|
value_type: Optional[str] # HA value_type for sensor/number, None otherwise
|
|
label: str # human-readable description
|
|
|
|
|
|
CATEGORY_LIGHTING = "lighting"
|
|
CATEGORY_SHUTTER = "shutter"
|
|
CATEGORY_HVAC = "hvac"
|
|
CATEGORY_SENSOR = "sensor"
|
|
CATEGORY_SCENE = "scene"
|
|
CATEGORY_ENERGY = "energy"
|
|
CATEGORY_DIAG = "diagnostics"
|
|
CATEGORY_UNKNOWN = "unknown"
|
|
|
|
KIND_COMMAND = "command"
|
|
KIND_STATUS = "status"
|
|
KIND_SENSOR = "sensor"
|
|
KIND_UNKNOWN = "unknown"
|
|
|
|
|
|
def dpt_key(main: Optional[int], sub: Optional[int]) -> str:
|
|
"""Return a normalized 'main.sub' / 'main' string, '' if no DPT."""
|
|
if main is None:
|
|
return ""
|
|
if sub is None:
|
|
return str(main)
|
|
return f"{main}.{sub:03d}"
|
|
|
|
|
|
def dpt_ets_token(main: Optional[int], sub: Optional[int]) -> str:
|
|
"""ETS GA-export 'DPTs' attribute token, e.g. DPST-1-1 or DPT-1."""
|
|
if main is None:
|
|
return ""
|
|
if sub is None:
|
|
return f"DPT-{main}"
|
|
return f"DPST-{main}-{sub}"
|
|
|
|
|
|
# Exact (main, sub) overrides. None sub = applies to whole main group as fallback.
|
|
_EXACT: dict[tuple[int, Optional[int]], DptInfo] = {
|
|
(1, 1): {"category": CATEGORY_LIGHTING, "kind": KIND_COMMAND, "ha_platform": "switch", "value_type": None, "label": "Switch on/off"},
|
|
(1, 2): {"category": CATEGORY_UNKNOWN, "kind": KIND_COMMAND, "ha_platform": "switch", "value_type": None, "label": "Boolean"},
|
|
(1, 3): {"category": CATEGORY_UNKNOWN, "kind": KIND_COMMAND, "ha_platform": "switch", "value_type": None, "label": "Enable"},
|
|
(1, 5): {"category": CATEGORY_DIAG, "kind": KIND_SENSOR, "ha_platform": "binary_sensor", "value_type": None, "label": "Alarm"},
|
|
(1, 8): {"category": CATEGORY_SHUTTER, "kind": KIND_COMMAND, "ha_platform": "cover", "value_type": None, "label": "Up/Down"},
|
|
(1, 9): {"category": CATEGORY_UNKNOWN, "kind": KIND_SENSOR, "ha_platform": "binary_sensor", "value_type": None, "label": "Open/Close"},
|
|
(1, 10): {"category": CATEGORY_SHUTTER, "kind": KIND_COMMAND, "ha_platform": "cover", "value_type": None, "label": "Start/Stop"},
|
|
(1, 11): {"category": CATEGORY_LIGHTING, "kind": KIND_STATUS, "ha_platform": "binary_sensor", "value_type": None, "label": "State (status)"},
|
|
(1, 18): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "binary_sensor", "value_type": None, "label": "Occupancy"},
|
|
(1, 19): {"category": CATEGORY_DIAG, "kind": KIND_SENSOR, "ha_platform": "binary_sensor", "value_type": None, "label": "Window/Door"},
|
|
(3, 7): {"category": CATEGORY_LIGHTING, "kind": KIND_COMMAND, "ha_platform": "light", "value_type": None, "label": "Dimming control (relative)"},
|
|
(3, 8): {"category": CATEGORY_SHUTTER, "kind": KIND_COMMAND, "ha_platform": "cover", "value_type": None, "label": "Blinds control (relative)"},
|
|
(5, 1): {"category": CATEGORY_LIGHTING, "kind": KIND_COMMAND, "ha_platform": "light", "value_type": "percent", "label": "Scaling 0-100% (brightness/position)"},
|
|
(5, 3): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "angle", "label": "Angle 0-360°"},
|
|
(5, 10): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "pulse", "label": "Counter pulses"},
|
|
(10, 1): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "datetime", "value_type": "time", "label": "Time of day"},
|
|
(11, 1): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "datetime", "value_type": "date", "label": "Date"},
|
|
(16, 0): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "text", "value_type": "string", "label": "Character string (ASCII)"},
|
|
(16, 1): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "text", "value_type": "string", "label": "Character string (ISO-8859-1)"},
|
|
(19, 1): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "datetime", "value_type": "datetime", "label": "Date and time"},
|
|
(7, 12): {"category": CATEGORY_ENERGY, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "electric_current", "label": "Current (mA)"},
|
|
(7, 13): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "illuminance", "label": "Brightness (lux)"},
|
|
(9, 1): {"category": CATEGORY_HVAC, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "temperature", "label": "Temperature (°C)"},
|
|
(9, 4): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "illuminance", "label": "Illuminance (lux)"},
|
|
(9, 5): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "wind_speed_ms", "label": "Wind speed (m/s)"},
|
|
(9, 7): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "humidity", "label": "Humidity (%)"},
|
|
(9, 8): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "ppm", "label": "Air quality (ppm)"},
|
|
(9, 20): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "voltage", "label": "Voltage (mV)"},
|
|
(9, 21): {"category": CATEGORY_ENERGY, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "current", "label": "Current (mA)"},
|
|
(12, 1): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "pulse_4_byte", "label": "Counter (4-byte unsigned)"},
|
|
(13, 10): {"category": CATEGORY_ENERGY, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "active_energy", "label": "Active energy (Wh)"},
|
|
(13, 13): {"category": CATEGORY_ENERGY, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "active_energy_kwh", "label": "Active energy (kWh)"},
|
|
(14, 19): {"category": CATEGORY_ENERGY, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "electric_current", "label": "Electric current (A)"},
|
|
(14, 27): {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "temperature", "label": "Temperature (K → °C)"},
|
|
(14, 56): {"category": CATEGORY_ENERGY, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "power", "label": "Power (W)"},
|
|
(17, 1): {"category": CATEGORY_SCENE, "kind": KIND_COMMAND, "ha_platform": "scene", "value_type": None, "label": "Scene number"},
|
|
(18, 1): {"category": CATEGORY_SCENE, "kind": KIND_COMMAND, "ha_platform": "scene", "value_type": None, "label": "Scene control"},
|
|
(20, 102): {"category": CATEGORY_HVAC, "kind": KIND_COMMAND, "ha_platform": "climate", "value_type": None, "label": "HVAC operation mode (comfort/standby/economy/frost)"},
|
|
(20, 105): {"category": CATEGORY_HVAC, "kind": KIND_COMMAND, "ha_platform": "climate", "value_type": None, "label": "HVAC controller mode (auto/heat/cool)"},
|
|
(1, 100): {"category": CATEGORY_HVAC, "kind": KIND_COMMAND, "ha_platform": "climate", "value_type": None, "label": "Heat/Cool"},
|
|
# Colour control — assembled into a light entity by the HA generator.
|
|
(7, 600): {"category": CATEGORY_LIGHTING, "kind": KIND_COMMAND, "ha_platform": "light", "value_type": None, "label": "Colour temperature (absolute K)"},
|
|
(232, 600): {"category": CATEGORY_LIGHTING, "kind": KIND_COMMAND, "ha_platform": "light", "value_type": None, "label": "RGB colour"},
|
|
(242, 600): {"category": CATEGORY_LIGHTING, "kind": KIND_COMMAND, "ha_platform": "light", "value_type": None, "label": "xyY colour"},
|
|
(251, 600): {"category": CATEGORY_LIGHTING, "kind": KIND_COMMAND, "ha_platform": "light", "value_type": None, "label": "RGBW colour"},
|
|
}
|
|
|
|
# Whole-main-group fallbacks when an exact (main, sub) is not known.
|
|
_MAIN_FALLBACK: dict[int, DptInfo] = {
|
|
1: {"category": CATEGORY_UNKNOWN, "kind": KIND_COMMAND, "ha_platform": "switch", "value_type": None, "label": "1-bit boolean"},
|
|
3: {"category": CATEGORY_LIGHTING, "kind": KIND_COMMAND, "ha_platform": "light", "value_type": None, "label": "4-bit relative control"},
|
|
5: {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "1byte_unsigned", "label": "1-byte unsigned"},
|
|
6: {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "1byte_signed", "label": "1-byte signed"},
|
|
7: {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "2byte_unsigned", "label": "2-byte unsigned"},
|
|
8: {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "2byte_signed", "label": "2-byte signed"},
|
|
9: {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "2byte_float", "label": "2-byte float"},
|
|
12: {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "4byte_unsigned", "label": "4-byte unsigned"},
|
|
13: {"category": CATEGORY_ENERGY, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "4byte_signed", "label": "4-byte signed"},
|
|
14: {"category": CATEGORY_SENSOR, "kind": KIND_SENSOR, "ha_platform": "sensor", "value_type": "4byte_float", "label": "4-byte float"},
|
|
17: {"category": CATEGORY_SCENE, "kind": KIND_COMMAND, "ha_platform": "scene", "value_type": None, "label": "Scene number"},
|
|
18: {"category": CATEGORY_SCENE, "kind": KIND_COMMAND, "ha_platform": "scene", "value_type": None, "label": "Scene control"},
|
|
20: {"category": CATEGORY_HVAC, "kind": KIND_COMMAND, "ha_platform": "climate", "value_type": None, "label": "1-byte HVAC enum"},
|
|
}
|
|
|
|
|
|
def is_exact_dpt(main: Optional[int], sub: Optional[int]) -> bool:
|
|
"""True when (main, sub) has an exact table entry — i.e. the category came
|
|
from the specific DPT, not from a whole-main-group fallback guess."""
|
|
return (main, sub) in _EXACT
|
|
|
|
|
|
def classify_dpt(main: Optional[int], sub: Optional[int]) -> DptInfo:
|
|
"""Classify a DPT into category / kind / HA platform."""
|
|
if main is None:
|
|
return {"category": CATEGORY_UNKNOWN, "kind": KIND_UNKNOWN,
|
|
"ha_platform": "unknown", "value_type": None, "label": "No DPT assigned"}
|
|
if (main, sub) in _EXACT:
|
|
return _EXACT[(main, sub)].copy() # copy
|
|
if main in _MAIN_FALLBACK:
|
|
return _MAIN_FALLBACK[main].copy()
|
|
return {"category": CATEGORY_UNKNOWN, "kind": KIND_UNKNOWN,
|
|
"ha_platform": "unknown", "value_type": None, "label": f"DPT {main}"}
|