Commit Graph

71 Commits

Author SHA1 Message Date
0ed67b34a0 added build-push.sh script for iot-bridge
Some checks failed
IoT Bridge - Tests & Quality Checks / Type Check (mypy) (push) Failing after 42s
IoT Bridge - Tests & Quality Checks / Tests (Python ${{ matrix.python-version }}) (3.10) (push) Failing after 16s
IoT Bridge - Tests & Quality Checks / Tests (Python ${{ matrix.python-version }}) (3.11) (push) Successful in 1m5s
IoT Bridge - Tests & Quality Checks / Tests (Python ${{ matrix.python-version }}) (3.12) (push) Successful in 1m9s
IoT Bridge - Tests & Quality Checks / Integration Tests (push) Failing after 18s
IoT Bridge - Tests & Quality Checks / Lint & Format Check (push) Failing after 4m49s
IoT Bridge - Tests & Quality Checks / Docker Build Test (push) Failing after 3m38s
IoT Bridge - Tests & Quality Checks / Quality Gate (push) Failing after 3s
2026-03-19 19:17:04 +01:00
0bce1e1bed fix(device-availability): fix timeout monitor and bridge-restart race conditions
- status_monitor: add availability_managed set; _monitor_loop skips devices
  in this set so the LWT/availability topic is the sole online/offline source
- device_manager: register device with status_monitor.set_availability_managed()
  so the monitor actually skips them (previously the monitor had no knowledge
  of DeviceManager.availability_managed)
- mqtt_bridge: remove blanket 'reset all devices to offline' on bridge restart;
  this was causing a race condition where the cron reset state AFTER the bridge
  had already sent device_online events via retained MQTT messages;
  stale running session cleanup is kept (still needed)
2026-03-19 19:11:03 +01:00
2fb45a6582 feat(device-manager): LWT-based availability monitoring for direct_session devices
- direct_session devices now use availability_topic (LWT) exclusively
  for online/offline state - timeout monitor no longer interferes
- Added availability_managed set: devices in this set bypass
  update_last_seen() and are ignored by the timeout monitor
- Added heartbeat_topics set: heartbeat messages return early before
  the session parser, eliminating direct_session_missing_fields warnings
- Added mark_online_silent() to DeviceStatusMonitor: updates state
  without emitting a duplicate device_online event
- registry.py: added availability_topic + status_topic params for
  direct_session parser type
- server.py: set last_config_update from file mtime on load_persisted_config
- mqtt_bridge.py: auto push config + reset device states when bridge
  comes back from offline (prevents stale state in Odoo after restart)
2026-03-19 18:58:55 +01:00
0293256c76 fix(iot_api): use savepoint to prevent InFailedSqlTransaction
When _process_event raised a DB exception (e.g. constraint violation),
PostgreSQL put the whole transaction into ABORTED state.  Any subsequent
ORM call (event.mark_processed, Odoo's own flush) then raised
psycopg2.errors.InFailedSqlTransaction, masking the real error.

Fix: wrap _process_event in a database savepoint inside receive_iot_event.
A processing failure now only rolls back the session/device side-effects;
the ows.iot.event record stays committed and the error is stored in
processing_error.  The transaction itself remains valid for Odoo's flush.
2026-03-19 17:03:04 +01:00
ffd1c2d10e fix: __manifest__.py data view order
Some checks failed
IoT Bridge - Tests & Quality Checks / Type Check (mypy) (push) Failing after 5m24s
IoT Bridge - Tests & Quality Checks / Tests (Python ${{ matrix.python-version }}) (3.10) (push) Failing after 55s
IoT Bridge - Tests & Quality Checks / Tests (Python ${{ matrix.python-version }}) (3.11) (push) Successful in 2m13s
IoT Bridge - Tests & Quality Checks / Lint & Format Check (push) Failing after 10m28s
IoT Bridge - Tests & Quality Checks / Integration Tests (push) Failing after 6s
IoT Bridge - Tests & Quality Checks / Tests (Python ${{ matrix.python-version }}) (3.12) (push) Successful in 2m23s
IoT Bridge - Tests & Quality Checks / Docker Build Test (push) Failing after 5m43s
IoT Bridge - Tests & Quality Checks / Quality Gate (push) Failing after 2s
2026-03-18 18:20:47 +01:00
97b3b0af7d docs: README.md 2026-03-17 21:51:47 +01:00
dbb2efab57 docs 2026-03-17 21:27:25 +01:00
fd540f5642 docs 2026-03-17 21:22:22 +01:00
3a1b442baa feat(open_workshop_pos_mqtt): Maschinenzeit-Popup verbessert
- Zeitfenster-Navigation mit 3 Schrittgrößen (±1h, ±4h, ±1 Tag)
- Sticky Navigationsheader: Zeitfenster-Steuerung bleibt beim Scrollen sichtbar
- Session-Liste scrollbar (max-height: 60vh)
- Alle/Keine-Button zum Selektieren aller Sessions
- Karten-Layout: Beginn/Ende/Total/Aktivzeit in 4-Spalten-Tabelle
- Datumsanzeige mit Sekunden
- Durationsformat M:SS (unter 60 min) / H:MM:SS h (ab 60 min)
- AlertDialog-Blocker entfernt: Popup öffnet sich auch bei leerer Session-Liste
- UTC-Fix in order_summary für korrekte Zeitberechnung
- README.md erstellt
- seed_workshop_sessions.py hinzugefügt (Testdaten Shaper Origin + Lasercutter)
2026-03-17 21:20:43 +01:00
0972124efe fix: NameError 'fields' in session_complete + fields/timedelta import
NameError: name 'fields' is not defined (ab 20:12 Uhr in den Logs)
Ursache: iot_api.py ist ein Controller, kein Model – 'fields' war
niemals importiert. Der UTC-Fix verwendete fälschlicherweise
fields.Datetime.to_datetime() und fields.Datetime.to_string().

Fix:
- 'fields' und 'timedelta' zu den top-level Imports ergänzt
- fields.Datetime.to_datetime(event.timestamp) → event.timestamp
  (ORM-Datetime-Felder sind im Controller bereits datetime-Objekte)
- fields.Datetime.to_string(dt) → dt.strftime('%Y-%m-%d %H:%M:%S')
- Inline 'from datetime import timedelta as _td' entfernt
2026-03-11 21:26:29 +01:00
5b62ef2224 fix: _logger.info() in session_complete mit Standard-Python-Logger-Syntax
_logger.info('msg', session_id=...) ist structlog-Syntax.
Python-Standard-Logger wirft dabei TypeError → Odoo rollback →
session_complete-Events wurden niemals committed und landeten
in der Bridge-Retry-Queue (Endlosschleife).

Fix: Auf _logger.info('msg %s %s', val1, val2)-Format umgestellt.
2026-03-11 21:21:04 +01:00
21ac8641cc fix: session_complete Zeitstempel UTC-korrekt + PoS attendanceEnd in UTC
Problem: Lasercutter-Sessions wurden in get_pos_session_suggestions
übersprungen, weil start_time > end_time.

Bug 1 – iot_api.py (session_complete):
session_start_time im Gerät-Payload ist Lokalzeit ohne TZ-Info.
Bisher naiv als UTC gespeichert → start_time lag 1h nach end_time.
Fix: start_time = end_time - session_seconds (komplett UTC-basiert).
session_start_time aus dem Payload wird nicht mehr verwendet.

Bug 2 – machine_time_control_button.js:
toServerDatetime(new Date()) verwendete getHours() (Lokalzeit).
Odoo erwartet UTC-Strings → attendanceEnd war 1h zu groß.
Fix: Auf getUTCHours() / getUTCDate() etc. umgestellt.
2026-03-11 21:04:28 +01:00
4fb98200b4 docs: README und API.md auf Stand 2026-03-11 aktualisiert
iot_bridge/README.md:
- Project Structure vervollständigt (bridge_types.py, dependencies.py,
  exceptions.py, pyproject.toml, requirements-dev.txt, core/bootstrap.py,
  core/service_manager.py, tests/conftest.py, tests/helpers.py,
  tests/fixtures/, tests/integration/ vollständig)

open_workshop_mqtt/README.md:
- Session Detection: Strategy A + B (Direct Session / session_complete)
- Device erstellen: parser_config JSON statt einzelner Threshold-Felder
- Event Types: session_complete, session_updated, heartbeat, power_change ergänzt
- Architecture: Availability-Topic + direktes Session-Event aufgeführt

open_workshop_mqtt/API.md:
- Neu generiert via build_docs.py (bisher 3, jetzt 5 Modelldateien:
  mqtt_bridge.py und mqtt_broker.py neu dazu)

open_workshop_mqtt/static/description/index.html:
- Neu generiert aus aktualisierter README.md
2026-03-11 20:06:57 +01:00
910f2d1112 feat: online/offline detection via availability topic (direct_session)
Lasercutter (direct_session) sendet Plain-Text 'online'/'offline' auf
<device-id>/availability – bislang wurden diese Nachrichten still verworfen.

Änderungen:
- mqtt_client.py: non-JSON Payloads als {'_raw': text} durchleiten
  (statt bei JSONDecodeError komplett zu verwerfen)
- device_manager._add_device: direct_session-Geräte abonnieren zusätzlich
  <device_id>/availability; Eintrag landet in device_map
- device_manager._remove_device: entfernt ALLE Topics eines Geräts
  (vorher nur das erste gefundene – Bug bei mehreren Topics)
- device_manager.route_message: {'_raw': 'online'/'offline'} erzeugt
  device_online / device_offline Event in der Queue (case-insensitive)
- 15 neue Unit-Tests in test_availability_pipeline.py (102/102 grün)
2026-03-11 15:39:36 +01:00
23e46efb41 docs: IMPLEMENTATION_PLAN aktualisiert auf Stand 2026-03-11
- Phase 1 + 2 als DONE markiert
- Tatsächliche Implementierung dokumentiert (parser_config JSON-Blob statt
  pro-Parser-Felder, _PARSER_SELECTION, Auto-Hint via onchange)
- Direct-Session-Architektur (Strategie B) dokumentiert
- E2E-Test-Ergebnisse Lasercutter ergänzt
- Test-Baseline von 46 auf 87 aktualisiert
- Neuen Abschnitt 'Neuen Parser integrieren' mit Schritt-für-Schritt-Checkliste hinzugefügt
2026-03-11 15:22:21 +01:00
b9485237f1 feat: end-to-end direct_session pipeline + auto hint view
Bridge → Odoo pipeline für direct_session parser:
- device_manager.py: route session_complete in event_queue (+ import uuid)
- iot_api.py: _process_event handler für session_complete
- iot_event.py: event_type += session_complete
- mqtt_session.py: end_reason += direct_session

View auto-hint aus Bridge-Registry:
- mqtt_device.py: parser_topic_hint + parser_type_description fields
- mqtt_device.py: _onchange_parser_type setzt beide Felder via GET /parsers
- mqtt_device_views.xml: einziger generischer alert-div (kein hardcoding mehr)
2026-03-11 15:16:28 +01:00
fcca3272ae feat: add DirectSessionParser for devices delivering complete sessions
Device payload (e.g. lasercutter/session):
  {session_id, session_minutes, session_seconds, session_start_time, freetime_s, ip}

- parsers/direct_session_parser.py: new parser returning message_type='session_complete'
- parsers/registry.py: register 'direct_session' with freetime_s parameter (default 0)
- mqtt_device.py: add ('direct_session', 'Direct Session (Lasercutter)') to _PARSER_SELECTION
- tests/unit/test_direct_session_parser.py: 24 tests (happy-path, missing fields, defaults, registry)
2026-03-11 14:29:22 +01:00
ea565775b2 feat: parser_config als Wire-Format durchgehend (Phase 3 komplett)
session_config wird nicht mehr über die API/YAML gesendet.
parser_config ist jetzt das einzige Config-Format zwischen Odoo und Bridge.

Änderungen:
- api/models.py: DeviceConfig.session_config → parser_config: dict
  SessionConfig bleibt als internes Modell für SessionDetector
  DeviceConfig.to_session_config() extrahiert Werte mit Defaults
- config/schema.py: DeviceConfig gleich umgestellt + to_session_config()
- config/loader.py: liest parser_config aus YAML, Fallback für legacy
  session_config (Rückwärtskompatibilität für bestehende config-active.yaml)
- core/device_manager.py: device.session_config → device.to_session_config()
- core/service_manager.py: session_config Referenzen entfernt
- Odoo _build_bridge_config: sendet parser_config direkt (+ heartbeat)
- Odoo iot_api.py: gleich umgestellt
- Tests: alle SessionConfig-Fixtures → parser_config dicts
  63/63 passing
2026-03-11 13:05:54 +01:00
907fce37da fix: ORM-Cache vor Bridge-Config-Push invalidieren
write() rief _build_bridge_config() auf bevor der ORM-Cache
geleert war – self.search() las noch die alten (gecachten) Werte.
Resultat: parser_type-Änderung wurde in die config-active.yaml
nicht übernommen.

Fix: flush_all() + invalidate_all() vor dem Push erzwingt,
dass _build_bridge_config() die soeben geschriebenen Werte liest.
2026-03-11 12:22:24 +01:00
c90fd5a6a8 revert: parser_config immer mit Defaults überschreiben bei Typwechsel
Der vorherige Fix war falsch: 'nur bei leerem Feld befüllen' führt
dazu, dass nach shelly_pm → dummy → shelly_pm die dummy-Config übrig
bleibt. Jeder Parser hat eigene Parameter, eine fremde Config ist
für den neuen Parser wertlos.

Korrekt: Typwechsel = immer frische Defaults des neuen Parsers.
2026-03-11 11:50:58 +01:00
b36eb491c2 fix: parser_config nur bei leerem Feld mit Defaults befüllen
Bisher wurden bei jedem parser_type-Wechsel die Registry-Defaults
überschrieben – angepasste Werte gingen verloren.

Neu: Defaults werden nur eingetragen wenn parser_config leer ('{}')
ist, z. B. bei neu angelegten Geräten oder nach manuellem Leeren.
2026-03-11 11:36:37 +01:00
8e7298fcbe fix: parser_config fields.Json → fields.Text für ace-Widget
Das ace-Widget erwartet einen String, aber fields.Json liefert ein
Python-Dict zurück. JS ruft .toString() darauf auf → '[object Object]'.

Änderungen:
- mqtt_device.py: fields.Json → fields.Text(default='{}')
- _onchange_parser_type: json.dumps(defaults, indent=2) statt dict
- get_parser_config_dict: json.loads() mit Fehlerbehandlung
- iot_api.py: json.loads(device.parser_config or '{}')
- DB: parser_config jsonb → text (USING parser_config::text)
2026-03-11 11:22:03 +01:00
d0ca48b3ef refactor(odoo): Registry-driven JSON Config (Phase 3)
Ersetzt alle parser-spezifischen Einzelfelder durch ein einziges
parser_config (fields.Json / jsonb) – neuer Parser = Bridge + 1 Zeile.

Entfernt (Odoo, DB):
  power_on_threshold_w, active_work_threshold_w
  startup_delay_s, shutdown_delay_s, message_timeout_s
  dummy_pulse_count, dummy_pulse_debounce_ms, dummy_reset_interval_min
  strategy_config (computed Text)
  session_strategy

Neu (Odoo, DB):
  parser_config (fields.Json / jsonb)

Geändert:
  _onchange_parser_type holt Defaults via GET /parsers aus
  Bridge-Registry, kein parser-spezifischer if/elif mehr
  _compute_strategy_config entfällt komplett
  _build_bridge_config liest parser_config direkt
  View: alle parser-spezifischen Gruppen weg, ein ace-Widget
  iot_api.py: strategy_config -> parser_config
  IMPLEMENTATION_PLAN.md: Phase 3 dokumentiert

Bridge: unverändert (session_config API bleibt, 63/63 Tests grün)
DB: parser_config jsonb-Spalte, alle alten Spalten entfernt
2026-03-11 10:56:32 +01:00
0111c5db24 feat(dummy_generic): Puls-Zähler-Parser als Referenz-Workflow
Demonstriert den vollständigen Ablauf für einen neuen Parser mit
ANDEREN Parametern als shelly_pm (Flat Wide Table, Bridge SoT).

Bridge (iot_bridge):
- parsers/dummy_parser.py: Komplett neu – liest 'pulses' (int) statt
  'value', gibt apower=float(pulses) für SessionDetector zurück
- parsers/registry.py: dummy_generic-Parameter ersetzt:
  standby_threshold_w/working_threshold_w → pulse_count, pulse_debounce_ms,
  reset_interval_min (je mit odoo_field-Mapping)

Odoo (open_workshop_mqtt):
- mqtt_device.py: 3 neue Felder dummy_pulse_count, dummy_pulse_debounce_ms,
  dummy_reset_interval_min (Flat Wide Table, NULL für andere Parser)
- mqtt_device.py: _compute_strategy_config depends + elif für dummy_generic
  erzeugt jetzt pulse-spezifischen JSON-Config-Dict
- mqtt_device.py: _onchange_parser_type setzt Puls-Defaults statt
  shelly-Schwellenwerte
- mqtt_device_views.xml: invisible-Bedingungen auf parser_type not in [...]
  umgestellt (korrekt skalierend bei 3+ Parsern)
- mqtt_device_views.xml: Inline-Hint-Divs aus Leistungs-Schwellenwerte
  entfernt (unnötig)
- mqtt_device_views.xml: Neue Gruppe '🔢 Puls-Konfiguration' mit 3 Feldern

DB: 3 neue Spalten in ows_mqtt_device angelegt (odoo -u Erfolg)
Tests: 63/63 grün
2026-03-10 19:30:43 +01:00
ab9ba5d270 test: Dummy-Parser als Erweiterungsvalidierung (Phase 2 Smoke-Test)
- parsers/dummy_parser.py: DummyParser – liest 'value' aus JSON-Payload,
  mappt auf apower; akzeptiert beliebige Topics
- parsers/registry.py: dummy_generic hinzugefügt (topic_hint: <device>/data)
- parsers/__init__.py: DummyParser exportiert
- mqtt_device.py: _PARSER_SELECTION um dummy_generic erweitert
- mqtt_device_views.xml: Warn-Block für dummy_generic (mit topic hint +
  Payload-Beispiel), nur sichtbar wenn parser_type == dummy_generic
- Validiert: GET /parsers liefert beide Typen, 63/63 Tests grün,
  Odoo Modul-Update ohne Fehler
2026-03-10 18:15:02 +01:00
c7915d0ba1 feat(odoo): Phase 2 – parser_type aus Bridge Registry ableiten
- mqtt_device.py: _PARSER_SELECTION-Konstante eingeführt; tasmota und
  generic entfernt (hatten keine Bridge-Implementierung); depends in
  _compute_strategy_config um parser_type erweitert; _onchange nutzt
  jetzt topic_hint aus Registry (<device>/status/pm1:0)
- mqtt_device_views.xml: Parser-spezifischer Info-Block für shelly_pm
  (topic_hint, Beschreibung) mit invisible='parser_type != shelly_pm'
- Bestehende DB-Geräte: alle 3 bereits auf shelly_pm → keine Migration
- GET /parsers: läuft und liefert vollständige Schema-Antwort
2026-03-10 18:03:12 +01:00
ddd1e05f55 feat(bridge): Dynamic Parser Registry – Phase 1
- parsers/registry.py: PARSER_REGISTRY mit shelly_pm-Eintrag; get_parser(),
  get_schema(), list_parser_types() als Public API
- parsers/__init__.py: Registry-Funktionen exportiert
- core/device_manager.py: globalen ShellyParser entfernt; DeviceManager
  verwaltet jetzt ein eigenes parser-Dict pro Device (per get_parser())
- api/server.py: GET /parsers Endpoint hinzugefügt (gibt get_schema() zurück)
- tests/unit/test_parser_registry.py: 17 neue Tests (Registry-API,
  PARSER_REGISTRY-Integrität, DeviceManager-Integration)
- tests/unit/test_device_manager.py: Test auf neues API angepasst (patch
  statt parser=-Argument)

Tests: 63/63 passed
2026-03-10 17:31:03 +01:00
4de3401416 fix: odoo/ als normales Verzeichnis tracken + Bugfix db_name + Feature-Docs
- git rm --cached odoo: Gitlink-Eintrag entfernt, odoo/ wird jetzt korrekt versioniert
- odoo/src/odoo-dev.conf: db_name = odoo gesetzt (behebt 404 bei mehreren DBs)
- docs/FEATURE_REQUEST_DYNAMIC_PARSER_REGISTRY.md: Feature Request inkl. ADR
- docs/IMPLEMENTATION_PLAN_DYNAMIC_PARSER_REGISTRY.md: 3-Phasen-Implementierungsplan
- iot_bridge/README.md: Doku aktualisiert
2026-03-10 17:17:59 +01:00
8d7be4b9a7 feat(pos-mqtt): produktgefilterte Sessions + sichtbares Orderline-Icon
- Maschinenzeit-Icon nur bei gemappten Produkten (OrderSummary Patch)

- Session-Suggestions auf gewählte Produkt-ID gefiltert

- Debug-Dialog bei leerem Zeitfenster inkl. Produkt/Von/Bis

- Asset-Switch auf order_summary Implementierung

- Implementierungsplan Schritt-4 Review ergänzt
2026-02-21 17:35:23 +01:00
f0881c3c2c feat: MVP Step 4 - PoS Maschinenzeit-Übernahme produktiv
- ControlButtons-Extension mit stabiler Template-Inheritance (hasclass XPath)
- MachineTimeSelectionPopup Komponente für Session-Auswahl und Minuten-Bearbeitung
- Backend Service get_pos_session_suggestions mit Overlap + Rounding
- Seed-Tool für reproducible Test-Sessions
- PoS-Mapping-Backend für Device-to-Product-Zuordnung
- Entfernt: fehleranfällige ProductScreen-Extension (Owl-Crash behoben)

Workflow im PoS:
1. Orderline auswählen
2. 'Maschinenzeit'-Button klicken (ControlButtons)
3. Session-Popup mit Vorschlägen + editierbaren Minuten
4. Auswahl bestätigen → Quantity in Orderline durch Summe ersetzen

Akzeptanzkriterien Step 4 erfüllt:
✓ Button sichtbar und erreichbar
✓ Popup öffnet und schließt korrekt
✓ Vorschläge sichtbar und auswählbar
✓ Übernahme schreibt in Auftrag
✓ 'Keine Sessions'-Hinweis bei Leerfall
✓ Keine Crashes mehr
2026-02-21 16:08:01 +01:00
2b7e15eb79 Merge branch 'iot-bridge' 2026-02-19 21:58:28 +01:00
2d483e08ad docs: consolidate root docs and archive historical requests 2026-02-19 21:57:07 +01:00
fb0710c680 fix(odoo): close stale running sessions on new starts 2026-02-19 20:42:52 +01:00
643da8036c docs: make API docs source-of-truth explicit 2026-02-19 20:08:47 +01:00
d6a8890340 docs: complete phase 4.4 public API docstrings 2026-02-19 19:27:32 +01:00
e2229fa137 Phase 4.3: Update project documentation
- Add ARCHITECTURE.md with component overview and runtime data flow
- Add DEVELOPMENT.md with local setup, testing, and debugging workflows
- Update README.md links and development documentation references
- Fix outdated API models reference in related docs section
- Update optimization plan status for Phase 4.3 progress
2026-02-19 19:19:44 +01:00
f5ad4ad7bf Phase 4.2: Add integration tests for reconnect, config push, and event delivery
- Add tests/integration/test_mqtt_reconnect.py with in-memory broker/client fakes
- Add tests/integration/test_config_push_integration.py for POST /config flow and reconnect trigger
- Add tests/integration/test_event_delivery.py for retry queue delivery behavior
- Add httpx dependency required by FastAPI/Starlette TestClient
- Update OPTIMIZATION_PLAN.md to mark 4.2 tasks complete

Validation:
- pytest tests/integration/test_mqtt_reconnect.py tests/integration/test_config_push_integration.py tests/integration/test_event_delivery.py
- mypy tests/integration/test_mqtt_reconnect.py tests/integration/test_config_push_integration.py tests/integration/test_event_delivery.py
2026-02-19 19:17:23 +01:00
8766597a6a Phase 4.1: Add missing unit tests for config and device lifecycle
- Add tests/unit/test_config.py for config loading and validation behavior
- Add tests/unit/test_device_manager.py for device add/remove/routing lifecycle
- Keep existing bootstrap/exceptions tests as completed items
- Update OPTIMIZATION_PLAN.md: Phase 4 status in progress, 4.1 test tasks checked

Validation:
- pytest tests/unit/test_config.py tests/unit/test_device_manager.py tests/unit/test_bootstrap.py tests/unit/test_exceptions.py
- mypy tests/unit/test_config.py tests/unit/test_device_manager.py
2026-02-19 19:14:04 +01:00
080a7275d9 Phase 3.3: Harden error handling and recovery patterns
- Add optional Odoo circuit-breaker for transient failures
- Unify timeout handling in Odoo and MQTT clients
- Improve transient error classification (timeout/connection/5xx/429)
- Add focused unit tests for recovery and circuit-breaker behavior
- Mark Phase 3.3 tasks as completed in optimization plan
2026-02-19 19:07:13 +01:00
f7b5a28f9a Fix: sync device status timeout with Odoo message timeout
- Include device_status_timeout_s in Odoo bridge payload
- Resolve status monitor timeout robustly with backward-compatible fallbacks
- Update running status monitor timeout on POST /config without restart
- Keep compatibility for legacy/local configs without device_status_timeout_s

Result: shaperorigin uses configured 90s timeout for online/offline monitor, preventing 30s flapping.
2026-02-19 18:40:30 +01:00
e723315e35 Fix: Unify device_status timeout with message_timeout_s
Problem: Device Status Monitor was using a hardcoded 30-second global timeout
for marking devices offline, independent of the configurable message_timeout_s.
This caused alternating offline/online events for devices with power=0 that
don't send frequent MQTT messages.

Solution: Use the same timeout value (message_timeout_s) for both:
1. Session Detection (message_timeout_s)
2. Device Status Monitoring (device_status_timeout_s)

Implementation:
- Add device_status_timeout_s field to api/models.py DeviceConfig (default: 120s)
- Update Odoo iot_api.py to include device_status_timeout_s in config response
  (synchronized with message_timeout_s from device strategy config)
- Update Bridge service_manager.py to use device_status_timeout_s when
  initializing DeviceStatusMonitor (fallback to global config if not provided)

Result:
- Single configurable timeout per device in Odoo
- Both checks (session + device status) use same value
- Backward compatible (defaults to 120s if not provided)
- Solves alternating offline/online events for low-power/idle devices

Validation:
- mypy: 0 errors across 47 files
- API model test: device_status_timeout_s field functional
2026-02-19 00:09:22 +01:00
ff4ef2f563 Phase 3: Complete type safety & logging unification (3.1-3.2)
Phase 3.1: Type Safety
- Add bridge_types.py for shared type aliases (EventDict, PowerWatts, Timestamp, DeviceID)
- Define protocols for callbacks and message parsers
- Strict type annotations on all core modules (session_detector, event_queue, device_manager)
- Fix Optional handling and type guards throughout codebase
- Achieve full mypy compliance: 0 errors across 47 source files

Phase 3.2: Logging Unification
- Migrate from stdlib logging to pure structlog across all runtime modules
- Convert all logs to structured event+fields format (snake_case event names)
- Remove f-string and printf-style logger calls
- Add contextvars support for per-request correlation
- Implement FastAPI middleware to bind request_id, http_method, http_path
- Propagate X-Request-ID header in responses
- Remove stdlib logging imports except setup layer (utils/logging.py)
- Ensure log-level consistency across all modules

Files Modified:
- iot_bridge/bridge_types.py (new) - Central type definitions
- iot_bridge/core/* - Type safety and logging unification
- iot_bridge/clients/* - Structured logging with request context
- iot_bridge/parsers/* - Type-safe parsing with structured logs
- iot_bridge/utils/logging.py - Pure structlog setup with contextvars
- iot_bridge/api/server.py - Added request correlation middleware
- iot_bridge/tests/* - Test fixtures updated for type safety
- iot_bridge/OPTIMIZATION_PLAN.md - Phase 3 status updated

Validation:
- mypy . → 0 errors (47 files)
- All unit tests pass
- Runtime behavior unchanged
- API response headers include X-Request-ID
2026-02-18 23:54:27 +01:00
4214a61b19 Complete phase 2 rest points: env overrides and unit tests 2026-02-18 23:30:33 +01:00
ca31e3f4a6 Update README.md 2026-02-18 23:29:36 +01:00
a4ea77e6b3 feat(phase2.4): add dependency injection container and factory wiring
Implemented Phase 2.4 (Dependency Injection Pattern):

- Added new dependencies module with DI container and runtime context
  - RuntimeContainer for injectable factories
  - RuntimeContext for resolved runtime objects
  - create_service_manager() factory
  - build_runtime_context() composition root
- Refactored main.py to use dependency container wiring
  - Main orchestration now resolves runtime via DI factories
  - Reduced direct constructor coupling in entrypoint
- Added unit tests for DI behavior with mocked dependencies
  - Verifies factory injection for service manager creation
  - Verifies runtime composition uses injected callables
- Updated optimization plan checkboxes for Phase 2.4

Validation:
- py_compile passed for new/changed files
- tests/unit/test_dependencies.py passed
- regression test test_event_queue::test_enqueue passed

Notes:
- Keeps existing runtime behavior unchanged
- Establishes clear composition root for future testability improvements
2026-02-18 23:17:22 +01:00
0c19bd35d0 docs(plan): Update optimization plan progress for phases 0-2
- Set overall status to in-progress
- Mark Phase 2 as partially completed
- Add implemented Phase 2 commit references
- Check off completed tasks for 2.1, 2.2 and 2.3
- Update timeline table (Phase 0/1 done, Phase 2 partial)

Note: Remaining open checkboxes now reflect work still pending (tests, DI, env hierarchy docs).
2026-02-18 23:13:39 +01:00
bad1d5d528 fix(runtime): restore MQTT reconnect reliability and persist API config state
- Recreate MQTT client on reconnect to apply broker/auth/TLS changes reliably
- Restart loop on new MQTT client instance after reconnect
- Track loop lifecycle to avoid stale client state
- Include MQTT section in initial ConfigServer current_config state
- Keep /config response consistent with persisted /data/config-active.yaml after restart

Result:
- Broker switches via Odoo push now connect reliably (including TLS/non-TLS changes)
- Bridge startup + persisted config reload now exposes mqtt data correctly via GET /config
- Event flow MQTT -> Bridge -> Odoo remains stable after container restarts
2026-02-18 23:10:12 +01:00
527b5645ed refactor(phase2.3): Migrate to Pydantic V2 and modernize config management
Complete Pydantic V2 migration and config modernization:

api/models.py:
- Migrated @validator → @field_validator (Pydantic V2)
- Added @classmethod decorators (required in V2)
- Updated validator signatures: (cls, v, values) → (cls, v, info)
- Consolidated unique validators into one method
- Fixed: 3 PydanticDeprecatedSince20 warnings 

config/schema.py:
- Migrated from @dataclass → Pydantic BaseModel
- Added Field() with validation constraints:
  * Port: ge=1, le=65535
  * Floats: gt=0, ge=0
  * Strings: min_length constraints
- Added descriptive field descriptions
- Better type safety and runtime validation

core/bootstrap.py:
- Replaced SimpleNamespace → MQTTConfig (Pydantic)
- Now returns properly typed Pydantic models
- Better IDE autocomplete and type checking

Benefits:
 No more Pydantic V1 deprecation warnings
 Runtime validation for all config fields
 Better error messages on invalid config
 Type-safe config throughout the application
 Automatic validation (port ranges, positive numbers, etc.)
 Prepared for Pydantic V3 migration

Migration verified:
- Config loading works: load_config('config.example.yaml') ✓
- Bootstrap works with Pydantic models ✓
- Field validation works (tested port ranges, thresholds) ✓
- All existing functionality preserved ✓

Test: python3 -c 'from core.bootstrap import bootstrap; bootstrap()'
 Works perfectly with new Pydantic V2 models

See OPTIMIZATION_PLAN.md Phase 2.3 for details.
2026-02-18 22:53:36 +01:00
eb3c49c8c4 refactor(phase2.2): Refactor main.py - Extract bootstrap and service_manager
Massive refactoring of main.py to improve code organization:

Files Created:
1. core/bootstrap.py (234 lines)
   - BootstrapConfig: Configuration container
   - parse_arguments(): CLI arg parsing
   - load_bridge_config(): Config loading with persisted fallback
   - setup_logger(): Structured logging setup
   - get_mqtt_config(): MQTT config with env var fallback
   - bootstrap(): Complete initialization orchestration

2. core/service_manager.py (369 lines)
   - ServiceManager: Central service lifecycle coordinator
   - Signal handlers for graceful shutdown
   - Service initialization (Odoo, MQTT, Event Queue, etc.)
   - Callbacks for config updates and MQTT reconnection
   - Graceful shutdown sequencing

Files Modified:
   - Now ultra-clean entry point
   - Only orchestration logic remains
   - 7-phase startup sequence clearly documented

2. core/__init__.py
   - Export new modules for clean imports

Benefits:
-  Single Responsibility: Each module has one clear purpose
-  Testability: Services can be tested independently
-  Readability: main.py is now self-documenting
-  Maintainability: Easy to find and modify specific logic
-  No global variables: All state in ServiceManager
-  Type hints throughout

Architecture:
  main.py (orchestration)
    ├── bootstrap() → config + logger
    └── ServiceManager() → all services
         ├── Odoo Client
         ├── Event Queue
         ├── Status Monitor
         ├── MQTT Client
         ├── Device Manager
         └── HTTP Config Server

Test: python3 main.py --config config.example.yaml
 Bridge starts successfully with all services

See OPTIMIZATION_PLAN.md Phase 2.2 for details.
2026-02-18 22:43:00 +01:00
548f94a652 refactor(exceptions): Erweitere Exception-Klassen mit vollständigen Implementierungen
Alle Exception-Klassen haben jetzt sinnvolle __init__-Methoden:

- ConfigurationError: path Parameter für Config-Dateipfad
- ConfigValidationError: field + value für fehlerhafte Felder
- ConnectionError: service Parameter (mqtt/odoo)
- MQTTConnectionError: broker + port Parameter
- DeviceError: device_id Parameter
- ValidationError: field + value für Validierungsfehler

Vorher: Klassen hatten nur 'pass' (technisch korrekt, aber wenig nützlich)
Nachher: Strukturierte Fehlerkontext-Erfassung mit dedizierten Attributen

Beispiel:
  # Alt:  raise ConfigurationError('File not found', details={'path': ...})
  # Neu:  raise ConfigurationError('File not found', path='/etc/config.yaml')

Angepasst:
- config/loader.py: Nutzt neuen path-Parameter statt details-Dict
- Alle bestehenden Aufrufe bleiben kompatibel (backward-compatible)

Test: python3 -c 'from exceptions import *; e = MQTTConnectionError(...)'
2026-02-18 22:33:37 +01:00