chore: initial baseline before refactoring
Initial snapshot of Home Assistant config (HAOS 2026.4.4)
prior to dashboard restructuring and helper YAML migration.
Pre-commit cleanup applied:
- Removed blueprints/switch_manager/ (9.4 MB stale, component already gone)
- Removed .storage/switch_manager
- Removed home-assistant.log.{1,old,fault}
- Removed zigbee2mqtt/configuration_backup_v{1..4}.yaml
- Removed zigbee2mqtt/database.db.tmp.2024-09-27 (1.5y old)
- Created empty themes/ to satisfy configuration.yaml include
.gitignore uses allowlist strategy for .storage/ to keep
all tokens, keys, and PINs out of version control.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
2026.4.4
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
# ============================================================
|
||||
# Home Assistant Config Repository – .gitignore
|
||||
# ============================================================
|
||||
# Strategy: Default-deny for sensitive areas.
|
||||
# .storage/* uses an explicit allowlist for files we DO want versioned.
|
||||
# ============================================================
|
||||
|
||||
# === Secrets (Top-Level) ===
|
||||
secrets.yaml
|
||||
*.key
|
||||
*.pem
|
||||
*.crt
|
||||
known_devices.yaml
|
||||
.uuid
|
||||
*credentials.json
|
||||
ps5-mqtt/credentials.json
|
||||
|
||||
# === Datenbanken & Logs ===
|
||||
home-assistant_v2.db*
|
||||
home-assistant.log*
|
||||
*.log
|
||||
*.log.*
|
||||
.ha_run.lock
|
||||
|
||||
# === Caches & Runtime ===
|
||||
tts/
|
||||
image/
|
||||
deps/
|
||||
.cloud/
|
||||
.cache/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
OZW_Log.txt
|
||||
|
||||
# === HACS / Frontend Community Cards ===
|
||||
www/community/
|
||||
|
||||
# === Custom Components – haben eigene Repos ===
|
||||
custom_components/
|
||||
|
||||
# === Z2M-Runtime (enthält Network Keys → SECRETS!) ===
|
||||
zigbee2mqtt/log/
|
||||
zigbee2mqtt/database.db
|
||||
zigbee2mqtt/database.db.tmp.*
|
||||
zigbee2mqtt/state.json
|
||||
zigbee2mqtt/coordinator_backup.json
|
||||
zigbee2mqtt/device_icons/
|
||||
|
||||
# === ESPHome Build Artefakte ===
|
||||
esphome/.esphome/
|
||||
esphome/.pioenvs/
|
||||
esphome/.piolibdeps/
|
||||
esphome/secrets.yaml
|
||||
|
||||
# === AppDaemon Runtime ===
|
||||
appdaemon/logs/
|
||||
appdaemon/.cache/
|
||||
|
||||
# === Backups (HAOS Backups, manuelle Snapshots) ===
|
||||
backup/
|
||||
backups/
|
||||
*.tar
|
||||
*.tar.gz
|
||||
|
||||
# === Editor / OS ===
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# === Claude Code projekt-lokale Settings ===
|
||||
.claude/settings.local.json
|
||||
.claude/file-history/
|
||||
.claude/sessions/
|
||||
|
||||
# ============================================================
|
||||
# .storage/ – ALLOWLIST-STRATEGIE
|
||||
# Default: alles ignorieren. Dann gezielt whitelisten.
|
||||
# ============================================================
|
||||
.storage/*
|
||||
.storage/**
|
||||
|
||||
# --- Verzeichnis selbst zulassen (sonst wirken die Whitelists nicht) ---
|
||||
!.storage/
|
||||
|
||||
# --- Whitelist: Dashboards & Lovelace (für Refactoring) ---
|
||||
!.storage/lovelace
|
||||
!.storage/lovelace.*
|
||||
!.storage/lovelace_dashboards
|
||||
!.storage/lovelace_resources
|
||||
!.storage/map
|
||||
|
||||
# --- Whitelist: Helper (für YAML-Migration) ---
|
||||
!.storage/input_boolean
|
||||
!.storage/input_button
|
||||
!.storage/input_datetime
|
||||
!.storage/input_number
|
||||
!.storage/input_select
|
||||
!.storage/input_text
|
||||
!.storage/timer
|
||||
!.storage/counter
|
||||
!.storage/schedule
|
||||
!.storage/scheduler.storage
|
||||
|
||||
# --- Whitelist: Registries (Areas/Floors/Labels — sicher, keine Tokens) ---
|
||||
!.storage/core.area_registry
|
||||
!.storage/core.floor_registry
|
||||
!.storage/core.label_registry
|
||||
!.storage/core.category_registry
|
||||
|
||||
# --- Whitelist: Energy-Dashboard-Konfig ---
|
||||
!.storage/energy
|
||||
|
||||
# --- Whitelist: Frontend-Panels & exposed_entities (UI-Struktur) ---
|
||||
!.storage/frontend_panels
|
||||
!.storage/homeassistant.exposed_entities
|
||||
|
||||
# --- Whitelist: Powercalc Group-Definitionen (Konfig, kein Secret) ---
|
||||
!.storage/powercalc_group
|
||||
|
||||
# ============================================================
|
||||
# WICHTIG – diese .storage-Files NIEMALS tracken (Tokens/Keys!):
|
||||
# core.config_entries → 28 OAuth-Tokens und API-Keys
|
||||
# core.device_registry → MACs, Seriennummern (Privacy)
|
||||
# core.entity_registry → groß, ändert sich ständig (Noise)
|
||||
# core.restore_state → Runtime-State, ändert sich sekündlich
|
||||
# core.config → Längengrad/Breitengrad (Privacy)
|
||||
# alarmo.storage → Alarm-PINs
|
||||
# androidtv_adbkey* → RSA Private Keys
|
||||
# esphome.* → ESPHome Verschlüsselungs-Keys
|
||||
# mobile_app → Push-Tokens
|
||||
# tapo_control_* → Tapo Auth
|
||||
# unifi_data → Unifi Auth
|
||||
# roborock/ → Roborock Auth
|
||||
# home_agent.* → AI-Memories (Privacy)
|
||||
# waste_collection_schedule.device_keys → API-Keys
|
||||
# nibe_energy_conversion_data_* → Werte (Daten, kein Config)
|
||||
# bluetooth.* → Bluetooth-Geräte-Liste
|
||||
# homekit_controller* → HomeKit-Pairing-Daten
|
||||
# hassio → Supervisor-Daten
|
||||
# trace.saved_traces → Trace-Logs
|
||||
# hacs.* → HACS-Cache
|
||||
# zone → Heim-Adresse/Koordinaten
|
||||
# assist_pipeline.pipelines → Cloud-API-Konfig
|
||||
# Bewusst NICHT in der Whitelist oben.
|
||||
# ============================================================
|
||||
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 9,
|
||||
"key": "core.area_registry",
|
||||
"data": {
|
||||
"areas": [
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-bathroom",
|
||||
"id": "badezimmer",
|
||||
"labels": [],
|
||||
"name": "Badezimmer",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2024-09-09T13:04:18.389639+00:00",
|
||||
"modified_at": "2025-08-17T07:32:57.160038+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": "sensor.buro_rauchmelder_humidity",
|
||||
"icon": "hue:room-office",
|
||||
"id": "buro",
|
||||
"labels": [],
|
||||
"name": "Büro",
|
||||
"picture": null,
|
||||
"temperature_entity_id": "sensor.buro_rauchmelder_temperature",
|
||||
"created_at": "2024-09-08T10:46:56.377433+00:00",
|
||||
"modified_at": "2025-08-17T07:35:11.809131+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "aussen",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-front-door",
|
||||
"id": "eingang",
|
||||
"labels": [],
|
||||
"name": "Eingang",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2025-03-03T15:42:03.555438+00:00",
|
||||
"modified_at": "2025-08-17T07:35:00.782090+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-dining",
|
||||
"id": "esszimmer",
|
||||
"labels": [],
|
||||
"name": "Esszimmer",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2024-09-11T08:18:34.627509+00:00",
|
||||
"modified_at": "2025-08-17T07:32:40.006548+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-other",
|
||||
"id": "gang",
|
||||
"labels": [],
|
||||
"name": "Gang",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2024-09-09T13:31:35.850691+00:00",
|
||||
"modified_at": "2025-08-17T07:32:30.684112+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "aussen",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-carport",
|
||||
"id": "garage",
|
||||
"labels": [],
|
||||
"name": "Garage",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2025-08-05T05:47:11.358780+00:00",
|
||||
"modified_at": "2025-08-05T05:47:11.358784+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-hallway",
|
||||
"id": "gaderobe",
|
||||
"labels": [],
|
||||
"name": "Garderobe",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2024-09-09T13:18:10.361439+00:00",
|
||||
"modified_at": "2025-08-17T07:31:31.671046+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "aussen",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-bbq",
|
||||
"id": "grillplatz",
|
||||
"labels": [],
|
||||
"name": "Grillplatz",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2025-08-20T17:18:48.577020+00:00",
|
||||
"modified_at": "2025-08-20T17:18:48.577024+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": "sensor.gastezimmer_rauchmelder_humidity",
|
||||
"icon": "hue:room-guestroom",
|
||||
"id": "gastezimmer",
|
||||
"labels": [],
|
||||
"name": "Gästezimmer",
|
||||
"picture": null,
|
||||
"temperature_entity_id": "sensor.gastezimmer_rauchmelder_temperature",
|
||||
"created_at": "2024-11-02T08:09:33.172900+00:00",
|
||||
"modified_at": "2025-08-17T07:30:05.628546+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": "sensor.kinderzimmer_rauchmelder_humidity",
|
||||
"icon": "hue:room-nursery",
|
||||
"id": "kinderzimmer",
|
||||
"labels": [],
|
||||
"name": "Kinderzimmer",
|
||||
"picture": null,
|
||||
"temperature_entity_id": "sensor.kinderzimmer_rauchmelder_temperature",
|
||||
"created_at": "2024-09-09T12:55:13.852546+00:00",
|
||||
"modified_at": "2025-08-17T07:33:16.734722+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-kitchen",
|
||||
"id": "kuche",
|
||||
"labels": [],
|
||||
"name": "Küche",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2024-09-08T10:30:15.825070+00:00",
|
||||
"modified_at": "2025-08-17T07:33:37.166858+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "aussen",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "mdi:spa",
|
||||
"id": "ruheplatz",
|
||||
"labels": [],
|
||||
"name": "Ruheplatz",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2025-08-05T05:45:12.026858+00:00",
|
||||
"modified_at": "2025-08-05T05:46:07.550591+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": "sensor.schlafzimmer_rauchmelder_humidity",
|
||||
"icon": "hue:room-bedroom",
|
||||
"id": "schlafzimmer",
|
||||
"labels": [],
|
||||
"name": "Schlafzimmer",
|
||||
"picture": null,
|
||||
"temperature_entity_id": "sensor.schlafzimmer_rauchmelder_temperature",
|
||||
"created_at": "2024-09-08T10:30:15.825093+00:00",
|
||||
"modified_at": "2025-08-17T07:33:44.481038+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-closet",
|
||||
"id": "speis",
|
||||
"labels": [],
|
||||
"name": "Speis",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2024-09-09T13:17:30.602321+00:00",
|
||||
"modified_at": "2025-08-17T07:35:26.021395+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": "sensor.technikraum_rauchmelder_humidity",
|
||||
"icon": "hue:room-garage",
|
||||
"id": "technikraum",
|
||||
"labels": [],
|
||||
"name": "Technikraum",
|
||||
"picture": null,
|
||||
"temperature_entity_id": "sensor.technikraum_rauchmelder_temperature",
|
||||
"created_at": "2024-09-08T10:48:15.405832+00:00",
|
||||
"modified_at": "2025-08-17T07:34:09.996450+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "aussen",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-porch",
|
||||
"id": "terrasse",
|
||||
"labels": [],
|
||||
"name": "Terrasse",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2024-12-14T11:33:19.621992+00:00",
|
||||
"modified_at": "2025-08-17T07:35:55.398434+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "hue:room-laundry",
|
||||
"id": "wc",
|
||||
"labels": [],
|
||||
"name": "WC",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2024-09-09T13:06:41.472709+00:00",
|
||||
"modified_at": "2025-08-17T07:34:36.025375+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"humidity_entity_id": "sensor.wohnzimmer_rauchmelder_humidity",
|
||||
"icon": "hue:room-living",
|
||||
"id": "wohnzimmer",
|
||||
"labels": [],
|
||||
"name": "Wohnzimmer",
|
||||
"picture": null,
|
||||
"temperature_entity_id": "sensor.wohnzimmer_rauchmelder_temperature",
|
||||
"created_at": "2024-09-08T10:30:15.824868+00:00",
|
||||
"modified_at": "2025-08-17T07:34:49.227873+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "aussen",
|
||||
"humidity_entity_id": null,
|
||||
"icon": "mdi:cannabis",
|
||||
"id": "grow",
|
||||
"labels": [],
|
||||
"name": "Grow",
|
||||
"picture": null,
|
||||
"temperature_entity_id": null,
|
||||
"created_at": "2026-04-13T06:45:41.915906+00:00",
|
||||
"modified_at": "2026-04-13T06:45:41.915909+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 2,
|
||||
"key": "core.category_registry",
|
||||
"data": {
|
||||
"categories": {
|
||||
"automation": [
|
||||
{
|
||||
"category_id": "01J99PMV3M2YG3FNGEH4XH7YHW",
|
||||
"created_at": "2024-10-03T17:25:36.500542+00:00",
|
||||
"icon": null,
|
||||
"modified_at": "2024-10-03T17:25:36.500547+00:00",
|
||||
"name": "Licht"
|
||||
},
|
||||
{
|
||||
"category_id": "01J99PNV8CSZW3EV091RMEJ8EZ",
|
||||
"created_at": "2024-10-03T17:26:09.420792+00:00",
|
||||
"icon": "mdi:timer",
|
||||
"modified_at": "2024-10-03T17:26:09.420797+00:00",
|
||||
"name": "Zeitsteuerung"
|
||||
},
|
||||
{
|
||||
"category_id": "01J99PPYR7R7QZJ24BS979KM8R",
|
||||
"created_at": "2024-10-03T17:26:45.767138+00:00",
|
||||
"icon": null,
|
||||
"modified_at": "2024-10-03T17:26:45.767145+00:00",
|
||||
"name": "Aqara H1 (decoupled and wireless)"
|
||||
},
|
||||
{
|
||||
"category_id": "01J99PQJM732WF5JC2NVVYAZDE",
|
||||
"created_at": "2024-10-03T17:27:06.119785+00:00",
|
||||
"icon": null,
|
||||
"modified_at": "2024-10-03T17:27:06.119791+00:00",
|
||||
"name": "Notifications"
|
||||
},
|
||||
{
|
||||
"category_id": "01K2VBACGMBJA0THY3YK7CM1GG",
|
||||
"created_at": "2025-08-17T06:26:24.916078+00:00",
|
||||
"icon": "mdi:hot-tub",
|
||||
"modified_at": "2025-08-17T06:26:24.916081+00:00",
|
||||
"name": "Whirlpool"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 3,
|
||||
"key": "core.floor_registry",
|
||||
"data": {
|
||||
"floors": [
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "erdgeschoss",
|
||||
"icon": null,
|
||||
"level": 0,
|
||||
"name": "Erdgeschoss",
|
||||
"created_at": "2024-09-14T09:36:39.934040+00:00",
|
||||
"modified_at": "2024-09-14T09:36:39.934045+00:00"
|
||||
},
|
||||
{
|
||||
"aliases": [],
|
||||
"floor_id": "aussen",
|
||||
"icon": "hue:room-outdoors",
|
||||
"level": null,
|
||||
"name": "Außen",
|
||||
"created_at": "2025-08-05T05:46:07.342525+00:00",
|
||||
"modified_at": "2025-08-05T05:46:07.342526+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 2,
|
||||
"key": "core.label_registry",
|
||||
"data": {
|
||||
"labels": [
|
||||
{
|
||||
"color": "deep-orange",
|
||||
"description": null,
|
||||
"icon": "mdi:smoke-detector",
|
||||
"label_id": "rauchmelder",
|
||||
"name": "Rauchmelder",
|
||||
"created_at": "2024-11-26T16:26:08.211237+00:00",
|
||||
"modified_at": "2024-11-26T16:26:08.211242+00:00"
|
||||
},
|
||||
{
|
||||
"color": "blue-grey",
|
||||
"description": null,
|
||||
"icon": "mdi:window-shutter",
|
||||
"label_id": "rolladen",
|
||||
"name": "Rolladen",
|
||||
"created_at": "2024-12-07T10:49:37.543488+00:00",
|
||||
"modified_at": "2024-12-07T10:49:37.543493+00:00"
|
||||
},
|
||||
{
|
||||
"color": "cyan",
|
||||
"description": null,
|
||||
"icon": "hue:friends-of-hue-senic",
|
||||
"label_id": "lichtschalter",
|
||||
"name": "Lichtschalter",
|
||||
"created_at": "2024-12-07T10:52:02.771486+00:00",
|
||||
"modified_at": "2024-12-07T10:52:02.771491+00:00"
|
||||
},
|
||||
{
|
||||
"color": "amber",
|
||||
"description": null,
|
||||
"icon": "mdi:home-lightbulb-outline",
|
||||
"label_id": "leuchte",
|
||||
"name": "Leuchte",
|
||||
"created_at": "2024-12-07T10:54:12.326880+00:00",
|
||||
"modified_at": "2024-12-07T10:54:12.326885+00:00"
|
||||
},
|
||||
{
|
||||
"color": "lime",
|
||||
"description": null,
|
||||
"icon": "mdi:motion-sensor",
|
||||
"label_id": "bewegungsmelder",
|
||||
"name": "Bewegungsmelder",
|
||||
"created_at": "2024-12-15T16:13:00.488482+00:00",
|
||||
"modified_at": "2024-12-15T16:13:10.814935+00:00"
|
||||
},
|
||||
{
|
||||
"color": null,
|
||||
"description": null,
|
||||
"icon": null,
|
||||
"label_id": "seasonal",
|
||||
"name": "Seasonal",
|
||||
"created_at": "2025-12-21T10:06:40.104210+00:00",
|
||||
"modified_at": "2025-12-21T10:06:40.104212+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "counter",
|
||||
"data": {
|
||||
"items": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 3,
|
||||
"key": "energy",
|
||||
"data": {
|
||||
"energy_sources": [
|
||||
{
|
||||
"type": "grid",
|
||||
"cost_adjustment_day": 0.0,
|
||||
"stat_energy_from": "sensor.tibber_pulse_zuhause_zahlerstand_verbrauch",
|
||||
"stat_cost": null,
|
||||
"entity_energy_price": "sensor.zuhause_strompreis",
|
||||
"number_energy_price": null,
|
||||
"stat_energy_to": "sensor.tibber_pulse_zuhause_zahlerstand_einspeisung",
|
||||
"stat_compensation": null,
|
||||
"entity_energy_price_export": null,
|
||||
"number_energy_price_export": 0.0695,
|
||||
"stat_rate": "sensor.tibber_pulse_zuhause_leistung"
|
||||
},
|
||||
{
|
||||
"type": "solar",
|
||||
"stat_energy_from": "sensor.powerocean_hj37zdh5zg5w0109_solarertrag_heute",
|
||||
"config_entry_solar_forecast": [
|
||||
"01J8CDN8M8JEDRF4BA6ENG0KQ7"
|
||||
],
|
||||
"stat_rate": "sensor.powerocean_mpptpv_pwrtotal"
|
||||
},
|
||||
{
|
||||
"type": "battery",
|
||||
"stat_energy_from": "sensor.battery_hj3azdh5zg3g0384_batterie_kumulierte_entladung",
|
||||
"stat_energy_to": "sensor.battery_hj3azdh5zg3g0384_batterie_kumulierte_ladung",
|
||||
"power_config": {
|
||||
"stat_rate": "sensor.battery_hj3azdh5zg3g0384_batterieleistung"
|
||||
},
|
||||
"stat_rate": "sensor.battery_hj3azdh5zg3g0384_batterieleistung"
|
||||
},
|
||||
{
|
||||
"type": "battery",
|
||||
"stat_energy_from": "sensor.battery_hj3azdh5zg3g0490_batterie_kumulierte_entladung",
|
||||
"stat_energy_to": "sensor.battery_hj3azdh5zg3g0490_batterie_kumulierte_ladung",
|
||||
"power_config": {
|
||||
"stat_rate": "sensor.battery_hj3azdh5zg3g0490_batterieleistung"
|
||||
},
|
||||
"stat_rate": "sensor.battery_hj3azdh5zg3g0490_batterieleistung"
|
||||
}
|
||||
],
|
||||
"device_consumption": [
|
||||
{
|
||||
"stat_consumption": "sensor.energy_log_used_energy_for_hot_water_over_the_past_hour_32294",
|
||||
"name": "Heißwasser"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.energy_log_used_energy_for_heating_over_the_past_hour_32292",
|
||||
"name": "Heizung"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.charger_ac31zeh4ag130052_ladeenergie_sitzung",
|
||||
"stat_rate": "sensor.charger_ac31zeh4ag130052_ladebox_leistung",
|
||||
"name": "Wallbox"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.wc_trockner_energy",
|
||||
"name": "Trockner"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.wc_waschmaschine_energy",
|
||||
"name": "Waschmaschine"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.badezimmer_handtuchheizung_device_energy",
|
||||
"name": "Handtuchheizung"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.kuche_geschirrspuler_energy",
|
||||
"name": "Geschirrspüler"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.kuche_kuhlschrank_energy",
|
||||
"name": "Kühlschrank"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.technikraum_netzwerkschrank_energy",
|
||||
"name": "Netzwerkschrank"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.whirlpool_energie",
|
||||
"stat_rate": "sensor.whirlpool_leistung",
|
||||
"name": "Whirlpool"
|
||||
},
|
||||
{
|
||||
"stat_consumption": "sensor.all_standby_energy",
|
||||
"stat_rate": "sensor.all_standby_power",
|
||||
"name": "Standby"
|
||||
}
|
||||
],
|
||||
"device_consumption_water": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "frontend_panels",
|
||||
"data": {
|
||||
"climate": {
|
||||
"show_in_sidebar": true
|
||||
},
|
||||
"light": {
|
||||
"show_in_sidebar": true
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "input_boolean",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "buro_licht_helper",
|
||||
"name": "Büro_Licht_Helper"
|
||||
},
|
||||
{
|
||||
"id": "geschirrspuler_uber_nacht",
|
||||
"icon": "mdi:dishwasher-alert",
|
||||
"name": "Geschirrspüler über Nacht"
|
||||
},
|
||||
{
|
||||
"id": "garderobe_helper",
|
||||
"name": "Garderobe Helper"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "input_button",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "nachtmodus",
|
||||
"name": "Nachtmodus",
|
||||
"icon": "mdi:weather-night"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "input_datetime",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "grow_startdatum",
|
||||
"name": "Grow Startdatum",
|
||||
"has_date": true,
|
||||
"has_time": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 2,
|
||||
"key": "input_select",
|
||||
"data": {
|
||||
"items": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "input_text",
|
||||
"data": {
|
||||
"items": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "lovelace.dashboard_grow",
|
||||
"data": {
|
||||
"config": {
|
||||
"views": [
|
||||
{
|
||||
"title": "Mission Control",
|
||||
"type": "sections",
|
||||
"max_columns": 2,
|
||||
"badges": [
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.spider_farmer_80f1b2b3b64a_temperature"
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.spider_farmer_80f1b2b3b64a_humidity"
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.spider_farmer_80f1b2b3b64a_vpd"
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_tent_power"
|
||||
}
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
"type": "grid",
|
||||
"cards": [
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Surveillance & Environment"
|
||||
},
|
||||
{
|
||||
"type": "custom:advanced-camera-card",
|
||||
"cameras": [
|
||||
{
|
||||
"camera_entity": "camera.tapo_c100_mainstream",
|
||||
"live_provider": "ha"
|
||||
}
|
||||
],
|
||||
"view": {
|
||||
"default": "live"
|
||||
},
|
||||
"menu": {
|
||||
"buttons": {
|
||||
"frigate": {
|
||||
"enabled": false
|
||||
},
|
||||
"timeline": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Plant Health"
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:stack-in-card",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "P1",
|
||||
"secondary": "{{ '⚠️' if is_state('plant.grow_plant_1', 'problem') else '✅' }}",
|
||||
"icon": "mdi:flower",
|
||||
"icon_color": "{{ 'red' if is_state('plant.grow_plant_1', 'problem') else 'green' }}",
|
||||
"layout": "vertical"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-chips-card",
|
||||
"alignment": "center",
|
||||
"chips": [
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_1_soil_moisture",
|
||||
"icon": "mdi:water-percent"
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_1_soil_fertility",
|
||||
"icon": "mdi:sprout"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "custom:stack-in-card",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "P2",
|
||||
"secondary": "{{ '⚠️' if is_state('plant.grow_plant_2', 'problem') else '✅' }}",
|
||||
"icon": "mdi:flower",
|
||||
"icon_color": "{{ 'red' if is_state('plant.grow_plant_2', 'problem') else 'green' }}",
|
||||
"layout": "vertical"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-chips-card",
|
||||
"alignment": "center",
|
||||
"chips": [
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_2_soil_moisture",
|
||||
"icon": "mdi:water-percent"
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_2_soil_fertility",
|
||||
"icon": "mdi:sprout"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "custom:stack-in-card",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "P3",
|
||||
"secondary": "{{ '⚠️' if is_state('plant.grow_plant_3', 'problem') else '✅' }}",
|
||||
"icon": "mdi:flower",
|
||||
"icon_color": "{{ 'red' if is_state('plant.grow_plant_3', 'problem') else 'green' }}",
|
||||
"layout": "vertical"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-chips-card",
|
||||
"alignment": "center",
|
||||
"chips": [
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_3_soil_moisture",
|
||||
"icon": "mdi:water-percent"
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_3_soil_fertility",
|
||||
"icon": "mdi:sprout"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "custom:stack-in-card",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "P4",
|
||||
"secondary": "{{ '⚠️' if is_state('plant.grow_plant_4', 'problem') else '✅' }}",
|
||||
"icon": "mdi:flower",
|
||||
"icon_color": "{{ 'red' if is_state('plant.grow_plant_4', 'problem') else 'green' }}",
|
||||
"layout": "vertical"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-chips-card",
|
||||
"alignment": "center",
|
||||
"chips": [
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_4_soil_moisture",
|
||||
"icon": "mdi:water-percent"
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_4_soil_fertility",
|
||||
"icon": "mdi:sprout"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "custom:stack-in-card",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "P5",
|
||||
"secondary": "{{ '⚠️' if is_state('plant.grow_plant_5', 'problem') else '✅' }}",
|
||||
"icon": "mdi:flower",
|
||||
"icon_color": "{{ 'red' if is_state('plant.grow_plant_5', 'problem') else 'green' }}",
|
||||
"layout": "vertical"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-chips-card",
|
||||
"alignment": "center",
|
||||
"chips": [
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_5_soil_moisture",
|
||||
"icon": "mdi:water-percent"
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "sensor.grow_plant_5_soil_fertility",
|
||||
"icon": "mdi:sprout"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "grid",
|
||||
"cards": [
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Secondary Environment"
|
||||
},
|
||||
{
|
||||
"type": "vertical-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:apexcharts-card",
|
||||
"graph_span": "48h",
|
||||
"header": {
|
||||
"show": true,
|
||||
"title": "48h Klima-Vergleich (Geglättet)"
|
||||
},
|
||||
"yaxis": [
|
||||
{
|
||||
"id": "temp",
|
||||
"min": 10,
|
||||
"max": 35,
|
||||
"apex_config": {
|
||||
"tickAmount": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hum",
|
||||
"opposite": true,
|
||||
"min": 20,
|
||||
"max": 100,
|
||||
"apex_config": {
|
||||
"tickAmount": 5
|
||||
}
|
||||
}
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"entity": "sensor.spider_farmer_80f1b2b3b64a_temperature",
|
||||
"name": "Zelt Temp",
|
||||
"yaxis_id": "temp",
|
||||
"stroke_width": 3,
|
||||
"color": "#00ff00",
|
||||
"group_by": {
|
||||
"func": "avg",
|
||||
"duration": "10m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"entity": "sensor.0x00158d0002c9fa8b_temperature",
|
||||
"name": "Außen Temp (ZB)",
|
||||
"yaxis_id": "temp",
|
||||
"stroke_width": 2,
|
||||
"color": "darkgreen",
|
||||
"group_by": {
|
||||
"func": "avg",
|
||||
"duration": "10m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"entity": "sensor.spider_farmer_80f1b2b3b64a_humidity",
|
||||
"name": "Zelt Hum",
|
||||
"yaxis_id": "hum",
|
||||
"stroke_width": 3,
|
||||
"color": "#00bfff",
|
||||
"group_by": {
|
||||
"func": "avg",
|
||||
"duration": "10m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"entity": "sensor.0x00158d0002c9fa8b_humidity",
|
||||
"name": "Außen Hum (ZB)",
|
||||
"yaxis_id": "hum",
|
||||
"stroke_width": 2,
|
||||
"color": "#1a4b6e",
|
||||
"group_by": {
|
||||
"func": "avg",
|
||||
"duration": "10m"
|
||||
}
|
||||
}
|
||||
],
|
||||
"apex_config": {
|
||||
"stroke": {
|
||||
"curve": "smooth",
|
||||
"dashArray": [
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
5
|
||||
]
|
||||
},
|
||||
"legend": {
|
||||
"show": true
|
||||
},
|
||||
"grid": {
|
||||
"borderColor": "#333",
|
||||
"strokeDashArray": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom:apexcharts-card",
|
||||
"graph_span": "48h",
|
||||
"header": {
|
||||
"show": true,
|
||||
"title": "48h VPD Verlauf (kPa)"
|
||||
},
|
||||
"yaxis": [
|
||||
{
|
||||
"id": "vpd",
|
||||
"min": 0,
|
||||
"max": 2.5,
|
||||
"decimals": 2,
|
||||
"apex_config": {
|
||||
"tickAmount": 5
|
||||
}
|
||||
}
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"entity": "sensor.spider_farmer_80f1b2b3b64a_vpd",
|
||||
"name": "Zelt VPD",
|
||||
"yaxis_id": "vpd",
|
||||
"stroke_width": 3,
|
||||
"color": "#ff9800",
|
||||
"group_by": {
|
||||
"func": "avg",
|
||||
"duration": "10m"
|
||||
}
|
||||
}
|
||||
],
|
||||
"apex_config": {
|
||||
"stroke": {
|
||||
"curve": "smooth"
|
||||
},
|
||||
"annotations": {
|
||||
"yaxis": [
|
||||
{
|
||||
"y": 0.8,
|
||||
"y2": 1.2,
|
||||
"borderColor": "#00ff00",
|
||||
"fillColor": "#00ff00",
|
||||
"opacity": 0.1,
|
||||
"label": {
|
||||
"text": "Optimum Veg",
|
||||
"style": {
|
||||
"background": "#00ff00",
|
||||
"color": "#000"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"legend": {
|
||||
"show": true
|
||||
},
|
||||
"grid": {
|
||||
"borderColor": "#333",
|
||||
"strokeDashArray": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "grid",
|
||||
"columns": 2,
|
||||
"square": false,
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "sensor.grow_env_sensor_1_temperature",
|
||||
"name": "S1 Innen",
|
||||
"icon_color": "orange"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "sensor.0x00158d0002c9fa8b_temperature",
|
||||
"name": "ZB Außen",
|
||||
"icon_color": "blue"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "sensor.grow_env_sensor_1_humidity",
|
||||
"name": "S1 Feuchte",
|
||||
"icon_color": "cyan"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "sensor.0x00158d0002c9fa8b_humidity",
|
||||
"name": "ZB Feuchte",
|
||||
"icon_color": "light-blue"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "sensor.grow_env_sensor_1_pressure",
|
||||
"name": "Druck In"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "sensor.0x00158d0002c9fa8b_pressure",
|
||||
"name": "Druck Out"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "grid",
|
||||
"cards": [
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Equipment & Controls"
|
||||
},
|
||||
{
|
||||
"type": "tile",
|
||||
"entity": "binary_sensor.spider_farmer_80f1b2b3b64a_growth_plan_running",
|
||||
"name": "Growth plan active",
|
||||
"vertical": false,
|
||||
"features_position": "bottom"
|
||||
},
|
||||
{
|
||||
"type": "tile",
|
||||
"entity": "sensor.spider_farmer_80f1b2b3b64a_growth_progress",
|
||||
"name": "Growth plan progress",
|
||||
"icon": "fa6-solid:percent",
|
||||
"vertical": false,
|
||||
"features_position": "bottom"
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-light-card",
|
||||
"entity": "light.spider_farmer_80f1b2b3b64a_primary_light",
|
||||
"name": "Light",
|
||||
"show_brightness_control": true,
|
||||
"use_light_color": true
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-fan-card",
|
||||
"entity": "fan.spider_farmer_80f1b2b3b64a_blower",
|
||||
"name": "Blower",
|
||||
"show_percentage_control": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-fan-card",
|
||||
"entity": "fan.spider_farmer_80f1b2b3b64a_fan",
|
||||
"name": "Fan",
|
||||
"tap_action": {
|
||||
"action": "more-info"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "fan.spider_farmer_80f1b2b3b64a_heater",
|
||||
"name": "Heater",
|
||||
"icon": "mdi:fire"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "glance",
|
||||
"entities": [
|
||||
{
|
||||
"entity": "sensor.grow_tent_power",
|
||||
"name": "Watt"
|
||||
},
|
||||
{
|
||||
"entity": "sensor.grow_tent_current",
|
||||
"name": "Ampere"
|
||||
},
|
||||
{
|
||||
"entity": "sensor.grow_tent_voltage",
|
||||
"name": "Volt"
|
||||
},
|
||||
{
|
||||
"entity": "binary_sensor.grow_tent_consumer_connected",
|
||||
"name": "Status"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "grid",
|
||||
"cards": [
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Grow Planer"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "input_datetime.grow_startdatum",
|
||||
"name": "Startdatum",
|
||||
"icon": "mdi:calendar-start"
|
||||
},
|
||||
{
|
||||
"type": "custom:stack-in-card",
|
||||
"cards": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"content": "{% set start_state = states('input_datetime.grow_startdatum') %} {% if start_state not in ['unknown', 'unavailable'] %}\n {% set start = as_timestamp(start_state) %}\n {% set heute = as_timestamp(now().strftime('%Y-%m-%d')) %}\n {% set tage = ((heute - start) / 86400) | int %}\n {% set start_dt = start_state | as_datetime %}\n ### Aktueller Tag: {{ tage }}\n | Phase | Zeitraum | Status |\n | :--- | :--- | :--- |\n | **Keimung** | bis {{ (start_dt + timedelta(days=5)).strftime('%d.%m.') }} | {{ '🟢 Aktiv' if tage <= 5 else '✅ OK' }} |\n | **Sämling** | bis {{ (start_dt + timedelta(days=19)).strftime('%d.%m.') }} | {{ '⏳' if tage < 6 else ('🟢 Aktiv' if tage <= 19 else '✅ OK') }} |\n | **Wachstum** | {{ (start_dt + timedelta(days=20)).strftime('%d.%m.') }} bis {{ (start_dt + timedelta(days=35)).strftime('%d.%m.') }} | {{ '⏳' if tage < 20 else ('🟢 Aktiv' if tage <= 35 else '✅ OK') }} |\n | **Blüte** | {{ (start_dt + timedelta(days=36)).strftime('%d.%m.') }} bis {{ (start_dt + timedelta(days=90)).strftime('%d.%m.') }} | {{ '⏳' if tage < 36 else ('🟢 Aktiv' if tage <= 90 else '✅ OK') }} |\n \n **Voraussichtliche Ernte: {{ (start_dt + timedelta(days=80)).strftime('%d.%m.%y') }}**\n ---\n **Sorten:** 2x Amnesia XXL, 2x Sticky Orange, 1x Purple Skunk\n{% endif %}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,498 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "lovelace.dashboard_spa",
|
||||
"data": {
|
||||
"config": {
|
||||
"views": [
|
||||
{
|
||||
"title": "Whirlpool",
|
||||
"type": "sections",
|
||||
"max_columns": 2,
|
||||
"path": "whirlpool",
|
||||
"icon": "mdi:hot-tub",
|
||||
"sections": [
|
||||
{
|
||||
"type": "grid",
|
||||
"cards": [
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Status & Steuerung"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{% set ph = states('sensor.whirlpool_ph') | float(0) %} {% set orp = states('sensor.whirlpool_oxydo_reduktionspotenzial') | float(0) %} {% set online = is_state('binary_sensor.isonline_2', 'on') %} {% if not online %} Offline ⚠️ {% elif ph >= 7.0 and ph <= 7.6 and orp >= 650 %} Badespaß bereit! 🛁 {% else %} Wasser prüfen! ⚠️ {% endif %}",
|
||||
"secondary": "{% set temp = states('sensor.spa_current_temperature') | float | round(1) %} {% set ziel = states('sensor.spa_desired_temperature') | float | round(1) %} {% set rate = states('sensor.whirlpool_heizrate') | float(0) | round(2) %} {{ temp }}°C → Ziel {{ ziel }}°C {%- if rate > 0 %} ↑ {{ rate }} °C/min {%- elif rate < 0 %} ↓ {{ rate | abs }} °C/min {%- endif %}",
|
||||
"icon": "mdi:hot-tub",
|
||||
"icon_color": "{% set ph = states('sensor.whirlpool_ph') | float(0) %} {% set orp = states('sensor.whirlpool_oxydo_reduktionspotenzial') | float(0) %} {% if not is_state('binary_sensor.isonline_2', 'on') %} grey {% elif ph >= 7.0 and ph <= 7.6 and orp >= 650 %} green {% elif (ph < 6.8 or ph > 7.9) or orp < 550 %} red {% else %} orange {% endif %}",
|
||||
"vertical": true,
|
||||
"grid_options": {
|
||||
"columns": 12,
|
||||
"rows": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-chips-card",
|
||||
"chips": [
|
||||
{
|
||||
"type": "template",
|
||||
"icon": "mdi:ph",
|
||||
"content": "{{ states(\"sensor.whirlpool_ph\") | float | round(1) }}",
|
||||
"icon_color": "blue"
|
||||
},
|
||||
{
|
||||
"type": "template",
|
||||
"icon": "mdi:water-percent",
|
||||
"content": "{{ states(\"sensor.whirlpool_oxydo_reduktionspotenzial\") | int }} mV",
|
||||
"icon_color": "purple"
|
||||
},
|
||||
{
|
||||
"type": "template",
|
||||
"icon": "mdi:flash",
|
||||
"content": "{{ states(\"sensor.whirlpool_leistung\") | int(0) }} W",
|
||||
"icon_color": "amber"
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"entity": "binary_sensor.isonline_2",
|
||||
"icon": "mdi:wifi"
|
||||
}
|
||||
],
|
||||
"alignment": "center"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-climate-card",
|
||||
"entity": "climate.spa_thermostat",
|
||||
"name": "Temperatur einstellen",
|
||||
"show_temperature_control": true,
|
||||
"layout": "horizontal"
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "switch.whirlpool",
|
||||
"name": "Strom",
|
||||
"icon": "mdi:power"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "switch.spa_pump",
|
||||
"name": "Düsen",
|
||||
"icon": "mdi:pump"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "switch.spa_light",
|
||||
"name": "Licht",
|
||||
"icon": "mdi:lightbulb-on"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "sensor.spa_filter_1",
|
||||
"name": "Filter 1"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "sensor.spa_ozone",
|
||||
"name": "UVC/Ozon"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "sensor.spa_circulation_pump",
|
||||
"name": "Zirkulation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-select-card",
|
||||
"entity": "select.spa_heater_mode",
|
||||
"name": "Heizmodus",
|
||||
"icon": "mdi:fire"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-select-card",
|
||||
"entity": "select.spa_temperature_range",
|
||||
"name": "Temp.-Bereich",
|
||||
"icon": "mdi:thermometer-lines"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{% set alerts = states('sensor.spa_total_alerts') | int(0) %} {% if alerts == 0 %} Keine Störungen {% else %} {{ alerts }} Warnung(en) aktiv {% endif %}",
|
||||
"secondary": "{{ states(\"sensor.spa_fault_message\") }}",
|
||||
"icon": "{% if states('sensor.spa_total_alerts') | int(0) > 0 %} mdi:alert-circle {% else %} mdi:check-circle {% endif %}",
|
||||
"icon_color": "{% if states('sensor.spa_total_alerts') | int(0) > 0 %} red {% else %} green {% endif %}",
|
||||
"fill_container": true,
|
||||
"grid_options": {
|
||||
"columns": 12,
|
||||
"rows": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Ambiente"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-light-card",
|
||||
"entity": "light.ruheplatz_lichterkette",
|
||||
"name": "Lichterkette",
|
||||
"show_brightness_control": true,
|
||||
"use_light_color": true
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-light-card",
|
||||
"entity": "light.ruheplatz_wegbeleuchtung",
|
||||
"name": "Wegbeleuchtung",
|
||||
"show_brightness_control": true,
|
||||
"use_light_color": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "grid",
|
||||
"cards": [
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Wasseranalyse (24h)"
|
||||
},
|
||||
{
|
||||
"type": "custom:apexcharts-card",
|
||||
"graph_span": "24h",
|
||||
"header": {
|
||||
"show": true,
|
||||
"show_states": true,
|
||||
"colorize_states": true
|
||||
},
|
||||
"yaxis": [
|
||||
{
|
||||
"id": "ph",
|
||||
"min": 6.5,
|
||||
"max": 8,
|
||||
"decimals": 1,
|
||||
"apex_config": {
|
||||
"tickAmount": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "orp",
|
||||
"min": 400,
|
||||
"max": 900,
|
||||
"opposite": true,
|
||||
"decimals": 0
|
||||
}
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"entity": "sensor.whirlpool_ph",
|
||||
"name": "pH",
|
||||
"yaxis_id": "ph",
|
||||
"color": "#2196f3",
|
||||
"stroke_width": 3
|
||||
},
|
||||
{
|
||||
"entity": "sensor.whirlpool_oxydo_reduktionspotenzial",
|
||||
"name": "ORP",
|
||||
"yaxis_id": "orp",
|
||||
"color": "#9c27b0",
|
||||
"stroke_width": 3
|
||||
}
|
||||
],
|
||||
"apex_config": {
|
||||
"annotations": {
|
||||
"position": "back",
|
||||
"yaxis": [
|
||||
{
|
||||
"y": 7,
|
||||
"y2": 7.6,
|
||||
"y_axis_id": "ph",
|
||||
"fillColor": "#4CAF50",
|
||||
"opacity": 0.15,
|
||||
"label": {
|
||||
"text": "IDEAL"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom:apexcharts-card",
|
||||
"graph_span": "24h",
|
||||
"header": {
|
||||
"show": true,
|
||||
"title": "Temperaturverlauf",
|
||||
"show_states": true,
|
||||
"colorize_states": true
|
||||
},
|
||||
"series": [
|
||||
{
|
||||
"entity": "sensor.spa_current_temperature",
|
||||
"name": "Ist",
|
||||
"color": "#f44336",
|
||||
"stroke_width": 3
|
||||
},
|
||||
{
|
||||
"entity": "sensor.spa_desired_temperature",
|
||||
"name": "Soll",
|
||||
"color": "#ff9800",
|
||||
"stroke_width": 2,
|
||||
"curve": "stepline"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "custom:apexcharts-card",
|
||||
"graph_span": "24h",
|
||||
"header": {
|
||||
"show": true,
|
||||
"title": "Verbrauch (W)",
|
||||
"show_states": true,
|
||||
"colorize_states": true
|
||||
},
|
||||
"series": [
|
||||
{
|
||||
"entity": "sensor.whirlpool_leistung",
|
||||
"name": "Gesamtleistung",
|
||||
"color": "#FF5722",
|
||||
"stroke_width": 2,
|
||||
"type": "area"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Energieverbrauch"
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{{ states('sensor.spa_heater_energy') | float | round(2) }} kWh",
|
||||
"secondary": "Heizung",
|
||||
"icon": "mdi:fire",
|
||||
"icon_color": "red"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{{ states('sensor.spa_pump_energy') | float | round(2) }} kWh",
|
||||
"secondary": "Düsen",
|
||||
"icon": "mdi:pump",
|
||||
"icon_color": "blue"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{{ states('sensor.spa_circulation_pump_energy') | float | round(2) }} kWh",
|
||||
"secondary": "Zirkulation",
|
||||
"icon": "mdi:water-sync",
|
||||
"icon_color": "cyan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Ondilo ICO Sensor"
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{{ states(\"sensor.whirlpool_batterie\") }} %",
|
||||
"secondary": "Sensor Akku",
|
||||
"icon": "mdi:battery-bluetooth",
|
||||
"icon_color": "{% set b = states('sensor.whirlpool_batterie') | int(100) %} {% if b < 20 %} red {% elif b < 50 %} orange {% else %} green {% endif %}"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{{ states(\"sensor.whirlpool_rssi\") }} dBm",
|
||||
"secondary": "Signal",
|
||||
"icon": "mdi:wifi",
|
||||
"icon_color": "{% set r = states('sensor.whirlpool_rssi') | int(-100) %} {% if r < -80 %} red {% elif r < -60 %} orange {% else %} green {% endif %}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "grid",
|
||||
"cards": [
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Pflege-Protokoll"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{% set last = states('input_datetime.whirlpool_letzte_wasserpflege') %} {% if last == 'unknown' or last == '' %}\n Noch kein Datum gesetzt\n{% else %}\n {% set days = ((now().date() - strptime(last[:10], '%Y-%m-%d').date()).days) %}\n Letzte Pflege: vor {{ days }} Tag{{ 'en' if days != 1 else '' }}\n{% endif %}",
|
||||
"secondary": "{% set last = states('input_datetime.whirlpool_letzte_wasserpflege') %} {% set ph = states('sensor.whirlpool_ph') | float(7.2) %} {% set orp = states('sensor.whirlpool_oxydo_reduktionspotenzial') | float(700) %} {% if last != 'unknown' and last != '' %}\n {% set days = ((now().date() - strptime(last[:10], '%Y-%m-%d').date()).days) %}\n {% if days >= 10 %} ⚠️ Pflege überfällig!\n {% elif days >= 7 %} Pflegerunde empfohlen\n {% else %} Nächste Pflege in {{ 7 - days }} Tagen\n {% endif %}\n{% else %}\n pH + Brom + Wasserrein einmal wöchentlich\n{% endif %} {%- set ph_ok = ph >= 7.0 and ph <= 7.6 %} {%- set orp_ok = orp >= 650 and orp <= 750 %} {{ ' | pH: ✓' if ph_ok else ' | pH: ⚠️ ' ~ ph | round(1) }} {{ ' | ORP: ✓' if orp_ok else ' | ORP: ⚠️ ' ~ orp | round(0) ~ ' mV' }}",
|
||||
"icon": "mdi:water-check",
|
||||
"icon_color": "{% set last = states('input_datetime.whirlpool_letzte_wasserpflege') %} {% set ph = states('sensor.whirlpool_ph') | float(7.2) %} {% set orp = states('sensor.whirlpool_oxydo_reduktionspotenzial') | float(700) %} {% if last == 'unknown' or last == '' %} grey {% elif ph < 6.8 or ph > 7.9 or orp < 550 %} red {% else %}\n {% set days = ((now().date() - strptime(last[:10], '%Y-%m-%d').date()).days) %}\n {% if days >= 10 %} red\n {% elif days >= 7 %} orange\n {% else %} green {% endif %}\n{% endif %}",
|
||||
"fill_container": true,
|
||||
"grid_options": {
|
||||
"columns": 12,
|
||||
"rows": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "input_datetime.whirlpool_letzte_wasserpflege",
|
||||
"name": "Wasserpflege – Datum setzen",
|
||||
"icon": "mdi:calendar-edit"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{% set last = states('input_datetime.whirlpool_letzter_kartuschenwechsel') %} {% if last == 'unknown' or last == '' %}\n Noch kein Datum gesetzt\n{% else %}\n {% set days = ((now().date() - strptime(last[:10], '%Y-%m-%d').date()).days) %}\n Letzter Kartuschenwechsel: vor {{ days }} Tag{{ 'en' if days != 1 else '' }}\n{% endif %}",
|
||||
"secondary": "{% set last = states('input_datetime.whirlpool_letzter_kartuschenwechsel') %} {% set intervall = 60 %} {% if last != 'unknown' and last != '' %}\n {% set days = ((now().date() - strptime(last[:10], '%Y-%m-%d').date()).days) %}\n {% set rest = intervall - days %}\n {% if rest <= 0 %} ⚠️ Kartusche wechseln! ({{ days }} Tage alt)\n {% elif rest <= 14 %} Wechsel in {{ rest }} Tagen empfohlen\n {% else %} Nächster Wechsel in {{ rest }} Tagen (alle 2 Monate)\n {% endif %}\n{% else %}\n Empfehlung: alle 60 Tage bei 1–2× Nutzung/Woche\n{% endif %}",
|
||||
"icon": "mdi:air-filter",
|
||||
"icon_color": "{% set last = states('input_datetime.whirlpool_letzter_kartuschenwechsel') %} {% if last == 'unknown' or last == '' %} grey {% else %}\n {% set days = ((now().date() - strptime(last[:10], '%Y-%m-%d').date()).days) %}\n {% if days >= 60 %} red\n {% elif days >= 46 %} orange\n {% else %} green {% endif %}\n{% endif %}",
|
||||
"fill_container": true,
|
||||
"grid_options": {
|
||||
"columns": 12,
|
||||
"rows": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "input_datetime.whirlpool_letzter_kartuschenwechsel",
|
||||
"name": "Kartuschenwechsel – Datum setzen",
|
||||
"icon": "mdi:calendar-edit"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "{% set last = states('input_datetime.whirlpool_letzter_wasserwechsel') %} {% if last == 'unknown' or last == '' %}\n Noch kein Datum gesetzt\n{% else %}\n {% set days = ((now().date() - strptime(last[:10], '%Y-%m-%d').date()).days) %}\n Letzter Wasserwechsel: vor {{ days }} Tag{{ 'en' if days != 1 else '' }}\n{% endif %}",
|
||||
"secondary": "{% set last = states('input_datetime.whirlpool_letzter_wasserwechsel') %} {% set intervall = 120 %} {% if last != 'unknown' and last != '' %}\n {% set days = ((now().date() - strptime(last[:10], '%Y-%m-%d').date()).days) %}\n {% set rest = intervall - days %}\n {% if rest <= 0 %} ⚠️ Wasserwechsel überfällig! ({{ days }} Tage alt)\n {% elif rest <= 21 %} Wasserwechsel in {{ rest }} Tagen planen\n {% else %} Nächster Wechsel in {{ rest }} Tagen (alle 4 Monate)\n {% endif %}\n{% else %}\n Empfehlung: alle 4 Monate bei 1–2× Nutzung/Woche\n{% endif %}",
|
||||
"icon": "mdi:water-sync",
|
||||
"icon_color": "{% set last = states('input_datetime.whirlpool_letzter_wasserwechsel') %} {% if last == 'unknown' or last == '' %} grey {% else %}\n {% set days = ((now().date() - strptime(last[:10], '%Y-%m-%d').date()).days) %}\n {% if days >= 120 %} red\n {% elif days >= 99 %} orange\n {% else %} green {% endif %}\n{% endif %}",
|
||||
"fill_container": true,
|
||||
"grid_options": {
|
||||
"columns": 12,
|
||||
"rows": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "input_datetime.whirlpool_letzter_wasserwechsel",
|
||||
"name": "Wasserwechsel – Datum setzen",
|
||||
"icon": "mdi:calendar-edit"
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Dosierungsempfehlung"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "Dosierung diese Woche",
|
||||
"secondary": "{% set ph = states('sensor.whirlpool_ph') | float(7.2) %} {% set orp = states('sensor.whirlpool_oxydo_reduktionspotenzial') | float(700) %}\n🧪 pH ({{ ph | round(1) }}): {% if ph >= 7.0 and ph <= 7.6 %} Optimal – nichts nötig. {% elif ph > 7.6 %}\n pH-Minus: ca. {{ (((ph - 7.6) / 0.1) * 10 / 1.1) | round(0) | int }} g zugeben.\n{% else %}\n pH-Plus: ca. {{ (((7.0 - ph) / 0.1) * 10) | round(0) | int }} g zugeben.\n{% endif %} Warte 4h, dann nachmessen. |\n💊 Brom ({{ orp | round(0) }} mV): {% if orp >= 650 and orp <= 750 %} Optimal – nichts nötig. {% elif orp > 750 %} Zu hoch – keine Tabletten, Deckel öffnen. {% elif orp < 600 %} 3–4 Tabletten in Dosierschwimmer. {% else %} 1–2 Tabletten nachlegen. {% endif %} |\n🫧 Wasserrein (Bayrol): 10 ml auf 1.500 l wöchentlich zugeben, Pumpe dabei 30 min laufen lassen.",
|
||||
"icon": "mdi:flask-outline",
|
||||
"icon_color": "{% set ph = states('sensor.whirlpool_ph') | float(7.2) %} {% set orp = states('sensor.whirlpool_oxydo_reduktionspotenzial') | float(700) %} {% if ph < 6.8 or ph > 7.9 or orp < 550 %} red {% elif (ph < 7.0 or ph > 7.6) or (orp < 650 or orp > 750) %} orange {% else %} green {% endif %}",
|
||||
"fill_container": true,
|
||||
"grid_options": {
|
||||
"columns": 12,
|
||||
"rows": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "Nach Wasserwechsel – Erstbefüllung",
|
||||
"secondary": "① 30 g Aktivsauerstoff (Bayrol Soft & Easy) direkt zugeben | ② Pumpe 1h laufen lassen | ③ pH messen & korrigieren (Ziel 7,0–7,6) | ④ Brom-Tabletten in Dosierschwimmer legen | ⑤ Nach 24h nochmals alle Werte prüfen",
|
||||
"icon": "mdi:flask",
|
||||
"icon_color": "blue",
|
||||
"fill_container": true,
|
||||
"grid_options": {
|
||||
"columns": 12,
|
||||
"rows": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-template-card",
|
||||
"primary": "Wöchentliche Pflege-Checkliste",
|
||||
"secondary": "① pH messen & ggf. korrigieren (Ziel 7,0–7,6) | ② Brom-Tabletten im Dosierschwimmer prüfen & ggf. nachlegen | ③ 10 ml Wasserrein (Bayrol) zugeben | ④ Pumpe mind. 30 min laufen lassen | ⑤ Filtration läuft? (mind. 4–6h/Tag) | ⑥ Abdeckung nach Nutzung schließen",
|
||||
"icon": "mdi:clipboard-check-outline",
|
||||
"icon_color": "teal",
|
||||
"fill_container": true,
|
||||
"grid_options": {
|
||||
"columns": 12,
|
||||
"rows": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "grid",
|
||||
"cards": [
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Filterprogramme"
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-select-card",
|
||||
"entity": "select.spa_filter_time_1",
|
||||
"name": "Filterzeit 1",
|
||||
"icon": "mdi:clock-start"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-select-card",
|
||||
"entity": "select.spa_filter_duration_1",
|
||||
"name": "Dauer 1",
|
||||
"icon": "mdi:timer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "horizontal-stack",
|
||||
"cards": [
|
||||
{
|
||||
"type": "custom:mushroom-select-card",
|
||||
"entity": "select.spa_filter_time_2",
|
||||
"name": "Filterzeit 2",
|
||||
"icon": "mdi:clock-start"
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-select-card",
|
||||
"entity": "select.spa_filter_duration_2",
|
||||
"name": "Dauer 2",
|
||||
"icon": "mdi:timer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "custom:mushroom-entity-card",
|
||||
"entity": "switch.whirlpool_filterzyklus_2_aktivieren",
|
||||
"name": "Filterzyklus 2 aktiv",
|
||||
"icon": "mdi:filter-check"
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"heading": "Zeitpläne & Automatik"
|
||||
},
|
||||
{
|
||||
"type": "custom:scheduler-card",
|
||||
"include": [
|
||||
"climate.spa_thermostat",
|
||||
"switch.spa_pump",
|
||||
"switch.whirlpool"
|
||||
],
|
||||
"title": "Whirlpool Automatik"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "lovelace.map",
|
||||
"data": {
|
||||
"config": {
|
||||
"strategy": {
|
||||
"type": "map"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "lovelace_dashboards",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "map",
|
||||
"icon": "mdi:map",
|
||||
"title": "Karte",
|
||||
"url_path": "map",
|
||||
"require_admin": false,
|
||||
"mode": "storage",
|
||||
"show_in_sidebar": true
|
||||
},
|
||||
{
|
||||
"id": "dashboard_haustechnik",
|
||||
"show_in_sidebar": true,
|
||||
"title": "Technik",
|
||||
"require_admin": false,
|
||||
"mode": "storage",
|
||||
"url_path": "dashboard-haustechnik",
|
||||
"icon": "mdi:wrench"
|
||||
},
|
||||
{
|
||||
"id": "lovelace",
|
||||
"icon": "mdi:view-dashboard",
|
||||
"title": "Übersicht",
|
||||
"url_path": "lovelace",
|
||||
"mode": "storage",
|
||||
"require_admin": false,
|
||||
"show_in_sidebar": true
|
||||
},
|
||||
{
|
||||
"id": "dashboard_grow",
|
||||
"show_in_sidebar": true,
|
||||
"icon": "mdi:cannabis",
|
||||
"title": "Grow",
|
||||
"require_admin": false,
|
||||
"mode": "storage",
|
||||
"url_path": "dashboard-grow"
|
||||
},
|
||||
{
|
||||
"id": "dashboard_spa",
|
||||
"show_in_sidebar": true,
|
||||
"icon": "mdi:spa",
|
||||
"title": "Spa",
|
||||
"require_admin": false,
|
||||
"mode": "storage",
|
||||
"url_path": "dashboard-spa"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "lovelace_resources",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "ea21d63a2ca2461d919284d86bea62c4",
|
||||
"url": "/hacsfiles/hass-hue-icons/hass-hue-icons.js?hacstag=3844345221253",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "860cd39e0b9b4a14a7a2287a38495c11",
|
||||
"url": "/hacsfiles/apexcharts-card/apexcharts-card.js?hacstag=331701152223",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "a55e416394c2425a9c813642a219c087",
|
||||
"url": "/hacsfiles/lovelace-auto-entities/auto-entities.js?hacstag=1677445841161",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "86772e61c8584e0081b0deefdf8bb03d",
|
||||
"url": "/hacsfiles/numberbox-card/numberbox-card.js?hacstag=304967918417",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "06ff0cbb5d0c4affbdcf4ad1d64aacaa",
|
||||
"url": "/hacsfiles/hassio-trash-card/trashcard.js?hacstag=678764124247",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "c0876f3fda434950bec3967dd6b0f4af",
|
||||
"url": "/hacsfiles/vacuum-card/vacuum-card.js?hacstag=2612912952120",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "a9711994054740c08f780a15e066a954",
|
||||
"url": "/webrtc/webrtc-camera.js?v=v3.6.0",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "2bc385f6fa2f496c9537c0507ac8ccc2",
|
||||
"url": "/hacsfiles/lovelace-xiaomi-vacuum-map-card/xiaomi-vacuum-map-card.js?hacstag=193372044232",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "f51ee9a679e74256a344b5b924862570",
|
||||
"url": "/hacsfiles/lovelace-mushroom/mushroom.js?hacstag=444350375511",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "c2d677a4e95a42189bb1af34bd44cc38",
|
||||
"url": "/hacsfiles/spotify-card-v2/spotify-card-v2.js?hacstag=88521021411",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "508c6e86a63e4d16acbf8e4e7aaaa10a",
|
||||
"url": "/hacsfiles/mini-media-player/mini-media-player-bundle.js?hacstag=14852083811611",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "11a657e713774a3191dd802c102c9fed",
|
||||
"url": "/hacsfiles/clock-weather-card/clock-weather-card.js?hacstag=522634019293",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "84f219107ed14b9d8618e9ce083c3e91",
|
||||
"url": "/hacsfiles/power-flow-card-plus/power-flow-card-plus.js?hacstag=618081815037",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "6621533b33ce4101a1367464c999de79",
|
||||
"url": "/hacsfiles/scheduler-card/scheduler-card.js?hacstag=2862701574017",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "993a69f87387426cb5712bbe8382555e",
|
||||
"url": "/hacsfiles/Bubble-Card/bubble-card.js?hacstag=680112919316",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "385152627dee4e749c262ce7af48e242",
|
||||
"url": "/hacsfiles/lovelace-fold-entity-row/fold-entity-row.js?hacstag=150781994232",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "78d6832cd0df4534ac0476ddeb07ede2",
|
||||
"url": "/hacsfiles/stack-in-card/stack-in-card.js?hacstag=248954055020",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "1a3f9968eb924fff990c9401c686ca83",
|
||||
"url": "/hacsfiles/advanced-camera-card/advanced-camera-card.js?hacstag=3940825527274",
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "c5057a3229b04e639c8a6bafcd8b9d7e",
|
||||
"url": "/hacsfiles/mass-player-card/mass-player-card.js?hacstag=1043277514260",
|
||||
"type": "module"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "map",
|
||||
"data": {
|
||||
"migrated": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": 2,
|
||||
"minor_version": 1,
|
||||
"key": "powercalc_group",
|
||||
"data": {
|
||||
"sensor.whirlpool_gesamtenergie_energy": {
|
||||
"sensor.spa_circulation_pump_energy": {"entity_id":"sensor.spa_circulation_pump_energy","state":"483.754","attributes":{"state_class":"total_increasing","last_update_time":"2026-04-27T13:09:03.212088","last_pump_state":"HIGH","unit_of_measurement":"kWh","device_class":"energy","icon":"mdi:lightning-bolt-circle","friendly_name":"Whirlpool Umwälzpumpe Energieverbrauch"},"last_changed":"2026-04-27T11:09:03.212113+00:00","last_reported":"2026-04-27T11:09:03.212113+00:00","last_updated":"2026-04-27T11:09:03.212113+00:00","context":{"id":"01KQ7A1RBCVAABXZ6N81GAJ2KH","parent_id":null,"user_id":null}},
|
||||
"sensor.spa_heater_energy": {"entity_id":"sensor.spa_heater_energy","state":"1584.904","attributes":{"state_class":"total_increasing","last_update_time":"2026-04-27T13:09:03.211779","last_heater_state":"OFF","unit_of_measurement":"kWh","device_class":"energy","icon":"mdi:lightning-bolt-circle","friendly_name":"Whirlpool Heater energy consumption"},"last_changed":"2026-04-27T11:00:02.830914+00:00","last_reported":"2026-04-27T11:09:03.211827+00:00","last_updated":"2026-04-27T11:09:03.211827+00:00","context":{"id":"01KQ7A1RBB3TWY00Z55TM1Y9N7","parent_id":null,"user_id":null}},
|
||||
"sensor.spa_pump_energy": {"entity_id":"sensor.spa_pump_energy","state":"14.837","attributes":{"state_class":"total_increasing","last_update_time":"2026-04-27T13:09:03.211995","last_pump_state":"OFF","unit_of_measurement":"kWh","device_class":"energy","icon":"mdi:lightning-bolt-circle","friendly_name":"Whirlpool Pumpenenergieverbrauch"},"last_changed":"2026-04-27T09:23:03.104558+00:00","last_reported":"2026-04-27T11:09:03.212024+00:00","last_updated":"2026-04-27T11:09:03.212024+00:00","context":{"id":"01KQ7A1RBCEJ4CSGN95A1STRSW","parent_id":null,"user_id":null}}
|
||||
},
|
||||
"sensor.whirlpool_gesamtenergie": {
|
||||
"sensor.spa_circulation_pump_energy": {"entity_id":"sensor.spa_circulation_pump_energy","state":"492.776","attributes":{"state_class":"total_increasing","last_update_time":"2026-04-30T13:03:25.823471","last_pump_state":"HIGH","unit_of_measurement":"kWh","device_class":"energy","icon":"mdi:lightning-bolt-circle","friendly_name":"Whirlpool Umwälzpumpe Energieverbrauch"},"last_changed":"2026-04-30T11:03:25.823496+00:00","last_reported":"2026-04-30T11:03:25.823496+00:00","last_updated":"2026-04-30T11:03:25.823496+00:00","context":{"id":"01KQF0XKVZ9GX58KQ04DFAS0BX","parent_id":null,"user_id":null}},
|
||||
"sensor.spa_heater_energy": {"entity_id":"sensor.spa_heater_energy","state":"1608.081","attributes":{"state_class":"total_increasing","last_update_time":"2026-04-30T13:03:25.823225","last_heater_state":"OFF","unit_of_measurement":"kWh","device_class":"energy","icon":"mdi:lightning-bolt-circle","friendly_name":"Whirlpool Heater energy consumption"},"last_changed":"2026-04-30T11:00:06.409674+00:00","last_reported":"2026-04-30T11:03:25.823259+00:00","last_updated":"2026-04-30T11:03:25.823259+00:00","context":{"id":"01KQF0XKVZ8B1XC1VXH3TP8AB1","parent_id":null,"user_id":null}},
|
||||
"sensor.spa_pump_energy": {"entity_id":"sensor.spa_pump_energy","state":"14.91","attributes":{"state_class":"total_increasing","last_update_time":"2026-04-30T13:03:25.823397","last_pump_state":"OFF","unit_of_measurement":"kWh","device_class":"energy","icon":"mdi:lightning-bolt-circle","friendly_name":"Whirlpool Pumpenenergieverbrauch"},"last_changed":"2026-04-30T10:19:25.584716+00:00","last_reported":"2026-04-30T11:03:25.823422+00:00","last_updated":"2026-04-30T11:03:25.823422+00:00","context":{"id":"01KQF0XKVZCFK29KV975QRCS1X","parent_id":null,"user_id":null}},
|
||||
"sensor.charger_ac31zeh4ag130052_evchargingenergy": {"entity_id":"sensor.charger_ac31zeh4ag130052_evchargingenergy","state":"0.0","attributes":{"state_class":"total_increasing","product_description":"evChargingEnergy","product_serial":"AC31ZEH4AG130052","unit_of_measurement":"Wh","device_class":"energy","friendly_name":"Powerpulse evChargingEnergy"},"last_changed":"2026-04-30T10:19:24.150827+00:00","last_reported":"2026-04-30T10:20:24.141960+00:00","last_updated":"2026-04-30T10:19:24.150827+00:00","context":{"id":"01KQEYD03PMX73BPJ96F8B7EM4","parent_id":null,"user_id":null}},
|
||||
"sensor.grow_tent_energy": {"entity_id":"sensor.grow_tent_energy","state":"59.38","attributes":{"state_class":"total_increasing","unit_of_measurement":"kWh","device_class":"energy","friendly_name":"Grow Tent Energie"},"last_changed":"2026-04-30T11:01:47.091434+00:00","last_reported":"2026-04-30T11:01:47.091434+00:00","last_updated":"2026-04-30T11:01:47.091434+00:00","context":{"id":"01KQF0TKEKET31BTMNA72TTVTX","parent_id":null,"user_id":null}},
|
||||
"sensor.whirlpool_energie": {"entity_id":"sensor.whirlpool_energie","state":"2096.010077","attributes":{"state_class":"total_increasing","unit_of_measurement":"kWh","device_class":"energy","friendly_name":"Whirlpool Energie"},"last_changed":"2026-04-30T11:02:59.995617+00:00","last_reported":"2026-04-30T11:02:59.995617+00:00","last_updated":"2026-04-30T11:02:59.995617+00:00","context":{"id":"01KQF0WTMVJRRZ7R8RRKM3AVSC","parent_id":null,"user_id":null}},
|
||||
"sensor.whirlpool_eingespeiste_energie": {"entity_id":"sensor.whirlpool_eingespeiste_energie","state":"0.0","attributes":{"state_class":"total_increasing","unit_of_measurement":"kWh","device_class":"energy","friendly_name":"Whirlpool Eingespeiste Energie"},"last_changed":"2026-04-30T10:58:42.003752+00:00","last_reported":"2026-04-30T10:58:42.003752+00:00","last_updated":"2026-04-30T10:58:42.003752+00:00","context":{"id":"01KQF0MYPK2K678MA5MYAENSW9","parent_id":null,"user_id":null}}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "schedule",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "whirlpool_heizung",
|
||||
"name": "Whirlpool Heizung",
|
||||
"icon": "mdi:hot-tub",
|
||||
"monday": [
|
||||
{
|
||||
"from": "09:00:00",
|
||||
"to": "16:00:00"
|
||||
}
|
||||
],
|
||||
"tuesday": [
|
||||
{
|
||||
"from": "09:00:00",
|
||||
"to": "16:00:00"
|
||||
}
|
||||
],
|
||||
"wednesday": [
|
||||
{
|
||||
"from": "09:00:00",
|
||||
"to": "16:00:00"
|
||||
}
|
||||
],
|
||||
"thursday": [
|
||||
{
|
||||
"from": "09:00:00",
|
||||
"to": "16:00:00"
|
||||
}
|
||||
],
|
||||
"friday": [
|
||||
{
|
||||
"from": "09:00:00",
|
||||
"to": "22:00:00"
|
||||
}
|
||||
],
|
||||
"saturday": [
|
||||
{
|
||||
"from": "09:00:00",
|
||||
"to": "22:00:00"
|
||||
}
|
||||
],
|
||||
"sunday": [
|
||||
{
|
||||
"from": "09:00:00",
|
||||
"to": "16:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"version": 3,
|
||||
"minor_version": 1,
|
||||
"key": "scheduler.storage",
|
||||
"data": {
|
||||
"schedules": [
|
||||
{
|
||||
"schedule_id": "a54f12",
|
||||
"timeslots": [
|
||||
{
|
||||
"start": "00:00:00",
|
||||
"stop": "10:00:00",
|
||||
"conditions": [],
|
||||
"condition_type": null,
|
||||
"track_conditions": false,
|
||||
"actions": [
|
||||
{
|
||||
"service": "climate.set_temperature",
|
||||
"entity_id": "climate.spa_thermostat",
|
||||
"service_data": {
|
||||
"hvac_mode": "heat",
|
||||
"temperature": 20
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"start": "10:00:00",
|
||||
"stop": "13:00:00",
|
||||
"conditions": [],
|
||||
"condition_type": null,
|
||||
"track_conditions": false,
|
||||
"actions": [
|
||||
{
|
||||
"service": "climate.set_temperature",
|
||||
"entity_id": "climate.spa_thermostat",
|
||||
"service_data": {
|
||||
"hvac_mode": "heat",
|
||||
"temperature": 40
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"start": "13:00:00",
|
||||
"stop": "16:00:00",
|
||||
"conditions": [],
|
||||
"condition_type": null,
|
||||
"track_conditions": false,
|
||||
"actions": [
|
||||
{
|
||||
"service": "climate.set_temperature",
|
||||
"entity_id": "climate.spa_thermostat",
|
||||
"service_data": {
|
||||
"temperature": 20,
|
||||
"hvac_mode": "heat"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"start": "16:00:00",
|
||||
"stop": "00:00:00",
|
||||
"conditions": [],
|
||||
"condition_type": null,
|
||||
"track_conditions": false,
|
||||
"actions": [
|
||||
{
|
||||
"service": "climate.set_temperature",
|
||||
"entity_id": "climate.spa_thermostat",
|
||||
"service_data": {
|
||||
"hvac_mode": "heat",
|
||||
"temperature": 20
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"weekdays": [
|
||||
"daily"
|
||||
],
|
||||
"start_date": null,
|
||||
"end_date": null,
|
||||
"repeat_type": "repeat",
|
||||
"name": null,
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
"key": "timer",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "timer_badezimmer_heizung",
|
||||
"name": "timer_badezimmer_heizung",
|
||||
"duration": "0:00:00",
|
||||
"restore": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
# =========================================================================
|
||||
# BÜRO
|
||||
# WS-EUK03 – operation_mode: decoupled ✓
|
||||
# =========================================================================
|
||||
buro_licht:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Büro Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.buro_decke
|
||||
mapping:
|
||||
single: toggle
|
||||
|
||||
buro_schreibtisch_switch:
|
||||
module: controllerx
|
||||
class: WXKG01LMLightController
|
||||
controller: Büro Schreibtischschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.buro_schreibtisch
|
||||
|
||||
# =========================================================================
|
||||
# WC
|
||||
# WS-EUK03 – operation_mode: decoupled ✓
|
||||
# WXKG15LM – operation_mode: event ✓
|
||||
# =========================================================================
|
||||
wc_licht:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: WC Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.wc_decke
|
||||
mapping:
|
||||
single: toggle
|
||||
double:
|
||||
- service: light.turn_off
|
||||
entity_id: light.wc_decke
|
||||
- service: light.turn_off
|
||||
entity_id: light.technikraum_decke
|
||||
|
||||
wc_wireless:
|
||||
module: controllerx
|
||||
class: WXKG15LMLightController
|
||||
controller: WC Wireless Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.wc_decke
|
||||
mapping:
|
||||
single_left:
|
||||
service: light.toggle
|
||||
entity_id: light.wc_decke
|
||||
single_right:
|
||||
service: light.toggle
|
||||
entity_id: light.wc_spiegel
|
||||
|
||||
# =========================================================================
|
||||
# BADEZIMMER
|
||||
# WS-EUK03 – operation_mode: decoupled ✓
|
||||
# WXKG15LM – operation_mode: event ✓
|
||||
# =========================================================================
|
||||
badezimmer_lichtschalter:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Badezimmer Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.badezimmer_decke
|
||||
mapping:
|
||||
single: toggle
|
||||
|
||||
badezimmer_h1w:
|
||||
module: controllerx
|
||||
class: WXKG15LMLightController
|
||||
controller: Badezimmer Wireless Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.badezimmer_decke
|
||||
mapping:
|
||||
single_left:
|
||||
service: light.toggle
|
||||
entity_id: light.badezimmer_decke
|
||||
single_right:
|
||||
service: light.toggle
|
||||
entity_id: light.badezimmer_spiegel
|
||||
|
||||
# =========================================================================
|
||||
# WOHNBEREICH & GANG
|
||||
# WS-EUK04 – operation_mode_left/right: decoupled ✓
|
||||
# =========================================================================
|
||||
wohnzimmer_garderobe_h1:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Gang Wohnzimmer Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.garderobe_spots
|
||||
mapping:
|
||||
single_left:
|
||||
service: light.toggle
|
||||
entity_id: light.garderobe_spots
|
||||
single_right:
|
||||
service: light.toggle
|
||||
entity_id: light.gang_spots
|
||||
|
||||
esszimmer_kuche_h1:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Wohnzimmer Küche Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.esszimmer_decke
|
||||
mapping:
|
||||
single_left:
|
||||
service: light.toggle
|
||||
entity_id: light.esszimmer_decke
|
||||
single_right:
|
||||
service: light.toggle
|
||||
entity_id: light.kuche_spots
|
||||
|
||||
wohnzimmer_gang_h1:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Esszimmer Gang Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.wohnzimmer_decke
|
||||
mapping:
|
||||
single_left:
|
||||
service: light.toggle
|
||||
entity_id: light.wohnzimmer_decke
|
||||
single_right:
|
||||
- service: light.toggle
|
||||
entity_id: light.gang_spots
|
||||
- service: light.toggle
|
||||
entity_id: light.wohnzimmer_spots
|
||||
|
||||
# WS-EUK03 – operation_mode: decoupled ✓
|
||||
gang_h1:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Gang Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.gang_spots
|
||||
mapping:
|
||||
single:
|
||||
- service: light.toggle
|
||||
entity_id: light.gang_spots
|
||||
- service: light.toggle
|
||||
entity_id: light.wohnzimmer_spots
|
||||
double:
|
||||
service: input_button.press
|
||||
entity_id: input_button.nachtmodus
|
||||
|
||||
wohnzimmer_button:
|
||||
module: controllerx
|
||||
class: WXKG01LMLightController
|
||||
controller: Wohnzimmer Button
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.wohnzimmer_decke
|
||||
mapping:
|
||||
single:
|
||||
service: switch.toggle
|
||||
entity_id: switch.wohnzimmer_steckdose
|
||||
double:
|
||||
service: input_button.press
|
||||
entity_id: input_button.nachtmodus
|
||||
|
||||
# =========================================================================
|
||||
# SCHLAFRÄUME, KINDERZIMMER & SPEIS
|
||||
# WS-EUK03 – decoupled ✓ (Gästezimmer & Speis noch setzen!)
|
||||
# =========================================================================
|
||||
kinderzimmer_bett:
|
||||
module: controllerx
|
||||
class: WXKG01LMLightController
|
||||
controller: Kinderzimmer Bettschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.kinderzimmer_spots
|
||||
|
||||
kinderzimmer_lichtschalter:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Kinderzimmer Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.kinderzimmer_spots
|
||||
mapping:
|
||||
single: toggle
|
||||
|
||||
schlafzimmer_h1:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Schlafzimmer Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.schlafzimmer_decke
|
||||
mapping:
|
||||
single: toggle
|
||||
|
||||
gastezimmer_h1:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Gästezimmer Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.gastezimmer_decke
|
||||
mapping:
|
||||
single: toggle
|
||||
|
||||
speis_h1:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Speis Lichtschalter
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.speis_decke
|
||||
mapping:
|
||||
single: toggle
|
||||
|
||||
# =========================================================================
|
||||
# TERRASSE & AUSSENBEREICH
|
||||
# =========================================================================
|
||||
|
||||
# WS-EUK03 – operation_mode: decoupled ✓
|
||||
esszimmer_terrasse:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Esszimmer Lichtschalter Terrasse
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.grillplatz_wandleuchte
|
||||
mapping:
|
||||
single: toggle
|
||||
|
||||
# WS-EUK03 – decoupled noch setzen!
|
||||
wohnzimmer_terrasse:
|
||||
module: controllerx
|
||||
class: LightController
|
||||
controller: Wohnzimmer Lichtschalter Terrasse
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.terrasse_wandleuchte
|
||||
mapping:
|
||||
single: toggle
|
||||
|
||||
terrasse_remote:
|
||||
module: controllerx
|
||||
class: TS0044LightController
|
||||
controller: Terrasse Fernbedienung
|
||||
integration:
|
||||
name: z2m
|
||||
listen_to: mqtt
|
||||
light: light.technikraum_decke
|
||||
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Create controller-based automations with ease to control your home devices and scenes.
|
||||
|
||||
https://github.com/xaviml/controllerx
|
||||
"""
|
||||
|
||||
from cx_core import (
|
||||
Controller,
|
||||
CoverController,
|
||||
LightController,
|
||||
MediaPlayerController,
|
||||
SwitchController,
|
||||
Z2MLightController,
|
||||
)
|
||||
from cx_devices.adeo import *
|
||||
from cx_devices.aqara import *
|
||||
from cx_devices.aurora import *
|
||||
from cx_devices.homematic import *
|
||||
from cx_devices.ikea import *
|
||||
from cx_devices.legrand import *
|
||||
from cx_devices.linkind import *
|
||||
from cx_devices.livarno import *
|
||||
from cx_devices.lutron import *
|
||||
from cx_devices.muller_licht import *
|
||||
from cx_devices.namron import *
|
||||
from cx_devices.osram import *
|
||||
from cx_devices.philips import *
|
||||
from cx_devices.prolight import *
|
||||
from cx_devices.rgb_genie import *
|
||||
from cx_devices.robb import *
|
||||
from cx_devices.sengled import *
|
||||
from cx_devices.shelly import *
|
||||
from cx_devices.smartkontakten import *
|
||||
from cx_devices.smartthings import *
|
||||
from cx_devices.sonoff import *
|
||||
from cx_devices.tasmota import *
|
||||
from cx_devices.terncy import *
|
||||
from cx_devices.trust import *
|
||||
from cx_devices.tuya import *
|
||||
@@ -0,0 +1,150 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
ActionFunction = Callable[..., Awaitable[Any]]
|
||||
ActionParams = tuple[Any, ...]
|
||||
ActionFunctionWithParams = tuple[ActionFunction, ActionParams]
|
||||
TypeAction = Union[ActionFunction, ActionFunctionWithParams]
|
||||
ActionEvent = Union[str, int]
|
||||
PredefinedActionsMapping = dict[str, TypeAction]
|
||||
DefaultActionsMapping = dict[ActionEvent, Optional[str]]
|
||||
|
||||
CustomAction = Union[str, dict[str, Any]]
|
||||
CustomActions = Union[list[CustomAction], CustomAction]
|
||||
CustomActionsMapping = dict[ActionEvent, Optional[CustomActions]]
|
||||
|
||||
Number = Union[int, float]
|
||||
|
||||
|
||||
class Light:
|
||||
ON = "on"
|
||||
OFF = "off"
|
||||
TOGGLE = "toggle"
|
||||
TOGGLE_FULL_BRIGHTNESS = "toggle_full_brightness"
|
||||
TOGGLE_FULL_WHITE_VALUE = "toggle_full_white_value"
|
||||
TOGGLE_FULL_COLOR_TEMP = "toggle_full_color_temp"
|
||||
TOGGLE_MIN_BRIGHTNESS = "toggle_min_brightness"
|
||||
TOGGLE_MIN_WHITE_VALUE = "toggle_min_white_value"
|
||||
TOGGLE_MIN_COLOR_TEMP = "toggle_min_color_temp"
|
||||
RELEASE = "release"
|
||||
ON_FULL_BRIGHTNESS = "on_full_brightness"
|
||||
ON_FULL_WHITE_VALUE = "on_full_white_value"
|
||||
ON_FULL_COLOR_TEMP = "on_full_color_temp"
|
||||
ON_MIN_BRIGHTNESS = "on_min_brightness"
|
||||
ON_MIN_WHITE_VALUE = "on_min_white_value"
|
||||
ON_MIN_COLOR_TEMP = "on_min_color_temp"
|
||||
ON_MIN_MAX_BRIGHTNESS = "on_min_max_brightness"
|
||||
ON_MAX_MIN_BRIGHTNESS = "on_max_min_brightness"
|
||||
ON_MIN_MAX_COLOR_TEMP = "on_min_max_color_temp"
|
||||
ON_MAX_MIN_COLOR_TEMP = "on_max_min_color_temp"
|
||||
SET_HALF_BRIGHTNESS = "set_half_brightness"
|
||||
SET_HALF_WHITE_VALUE = "set_half_white_value"
|
||||
SET_HALF_COLOR_TEMP = "set_half_color_temp"
|
||||
SYNC = "sync"
|
||||
CLICK = "click"
|
||||
CLICK_BRIGHTNESS_UP = "click_brightness_up"
|
||||
CLICK_BRIGHTNESS_DOWN = "click_brightness_down"
|
||||
CLICK_WHITE_VALUE_UP = "click_white_value_up"
|
||||
CLICK_WHITE_VALUE_DOWN = "click_white_value_down"
|
||||
CLICK_COLOR_UP = "click_color_up"
|
||||
CLICK_COLOR_DOWN = "click_color_down"
|
||||
CLICK_COLOR_TEMP_UP = "click_colortemp_up"
|
||||
CLICK_COLOR_TEMP_DOWN = "click_colortemp_down"
|
||||
CLICK_XY_COLOR_UP = "click_xycolor_up"
|
||||
CLICK_XY_COLOR_DOWN = "click_xycolor_down"
|
||||
HOLD = "hold"
|
||||
HOLD_BRIGHTNESS_UP = "hold_brightness_up"
|
||||
HOLD_BRIGHTNESS_DOWN = "hold_brightness_down"
|
||||
HOLD_BRIGHTNESS_TOGGLE = "hold_brightness_toggle"
|
||||
HOLD_WHITE_VALUE_UP = "hold_white_value_up"
|
||||
HOLD_WHITE_VALUE_DOWN = "hold_white_value_down"
|
||||
HOLD_WHITE_VALUE_TOGGLE = "hold_white_value_toggle"
|
||||
HOLD_COLOR_UP = "hold_color_up"
|
||||
HOLD_COLOR_DOWN = "hold_color_down"
|
||||
HOLD_COLOR_TOGGLE = "hold_color_toggle"
|
||||
HOLD_COLOR_TEMP_UP = "hold_colortemp_up"
|
||||
HOLD_COLOR_TEMP_DOWN = "hold_colortemp_down"
|
||||
HOLD_COLOR_TEMP_TOGGLE = "hold_colortemp_toggle"
|
||||
HOLD_XY_COLOR_UP = "hold_xycolor_up"
|
||||
HOLD_XY_COLOR_DOWN = "hold_xycolor_down"
|
||||
HOLD_XY_COLOR_TOGGLE = "hold_xycolor_toggle"
|
||||
XYCOLOR_FROM_CONTROLLER = "xycolor_from_controller"
|
||||
COLORTEMP_FROM_CONTROLLER = "colortemp_from_controller"
|
||||
COLORTEMP_FROM_CONTROLLER_STEP = "colortemp_from_controller_step"
|
||||
BRIGHTNESS_FROM_CONTROLLER_LEVEL = "brightness_from_controller_level"
|
||||
BRIGHTNESS_FROM_CONTROLLER_ANGLE = "brightness_from_controller_angle"
|
||||
BRIGHTNESS_FROM_CONTROLLER_STEP = "brightness_from_controller_step"
|
||||
|
||||
|
||||
class Z2MLight:
|
||||
ON = "on"
|
||||
OFF = "off"
|
||||
TOGGLE = "toggle"
|
||||
RELEASE = "release"
|
||||
ON_FULL_BRIGHTNESS = "on_full_brightness"
|
||||
ON_FULL_COLOR_TEMP = "on_full_color_temp"
|
||||
ON_MIN_BRIGHTNESS = "on_min_brightness"
|
||||
ON_MIN_COLOR_TEMP = "on_min_color_temp"
|
||||
SET_HALF_BRIGHTNESS = "set_half_brightness"
|
||||
SET_HALF_COLOR_TEMP = "set_half_color_temp"
|
||||
CLICK = "click"
|
||||
CLICK_BRIGHTNESS_UP = "click_brightness_up"
|
||||
CLICK_BRIGHTNESS_DOWN = "click_brightness_down"
|
||||
CLICK_COLOR_TEMP_UP = "click_colortemp_up"
|
||||
CLICK_COLOR_TEMP_DOWN = "click_colortemp_down"
|
||||
HOLD = "hold"
|
||||
HOLD_BRIGHTNESS_UP = "hold_brightness_up"
|
||||
HOLD_BRIGHTNESS_DOWN = "hold_brightness_down"
|
||||
HOLD_BRIGHTNESS_TOGGLE = "hold_brightness_toggle"
|
||||
HOLD_COLOR_TEMP_UP = "hold_colortemp_up"
|
||||
HOLD_COLOR_TEMP_DOWN = "hold_colortemp_down"
|
||||
HOLD_COLOR_TEMP_TOGGLE = "hold_colortemp_toggle"
|
||||
XYCOLOR_FROM_CONTROLLER = "xycolor_from_controller"
|
||||
COLORTEMP_FROM_CONTROLLER = "colortemp_from_controller"
|
||||
BRIGHTNESS_FROM_CONTROLLER_LEVEL = "brightness_from_controller_level"
|
||||
BRIGHTNESS_FROM_CONTROLLER_ANGLE = "brightness_from_controller_angle"
|
||||
|
||||
|
||||
class MediaPlayer:
|
||||
HOLD_VOLUME_DOWN = "hold_volume_down"
|
||||
HOLD_VOLUME_UP = "hold_volume_up"
|
||||
CLICK_VOLUME_DOWN = "click_volume_down"
|
||||
CLICK_VOLUME_UP = "click_volume_up"
|
||||
VOLUME_SET = "volume_set"
|
||||
RELEASE = "release"
|
||||
PLAY = "play"
|
||||
PAUSE = "pause"
|
||||
PLAY_PAUSE = "play_pause"
|
||||
NEXT_TRACK = "next_track"
|
||||
PREVIOUS_TRACK = "previous_track"
|
||||
NEXT_SOURCE = "next_source"
|
||||
PREVIOUS_SOURCE = "previous_source"
|
||||
MUTE = "mute"
|
||||
TTS = "tts"
|
||||
VOLUME_FROM_CONTROLLER_ANGLE = "volume_from_controller_angle"
|
||||
|
||||
|
||||
class Switch:
|
||||
ON = "on"
|
||||
OFF = "off"
|
||||
TOGGLE = "toggle"
|
||||
|
||||
|
||||
class Cover:
|
||||
OPEN = "open"
|
||||
CLOSE = "close"
|
||||
STOP = "stop"
|
||||
TOGGLE_OPEN = "toggle_open"
|
||||
TOGGLE_CLOSE = "toggle_close"
|
||||
|
||||
|
||||
class StepperDir:
|
||||
UP = "up"
|
||||
DOWN = "down"
|
||||
TOGGLE = "toggle"
|
||||
|
||||
|
||||
class StepperMode:
|
||||
STOP = "stop"
|
||||
LOOP = "loop"
|
||||
BOUNCE = "bounce"
|
||||
@@ -0,0 +1,18 @@
|
||||
from cx_core.controller import Controller, action
|
||||
from cx_core.release_hold_controller import ReleaseHoldController
|
||||
from cx_core.type.cover_controller import CoverController
|
||||
from cx_core.type.light_controller import LightController
|
||||
from cx_core.type.media_player_controller import MediaPlayerController
|
||||
from cx_core.type.switch_controller import SwitchController
|
||||
from cx_core.type.z2m_light_controller import Z2MLightController
|
||||
|
||||
__all__ = [
|
||||
"Controller",
|
||||
"ReleaseHoldController",
|
||||
"LightController",
|
||||
"Z2MLightController",
|
||||
"MediaPlayerController",
|
||||
"SwitchController",
|
||||
"CoverController",
|
||||
"action",
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from cx_const import ActionEvent, CustomAction, CustomActions
|
||||
from cx_core.action_type.base import ActionType
|
||||
from cx_core.action_type.call_service_action_type import CallServiceActionType
|
||||
from cx_core.action_type.delay_action_type import DelayActionType
|
||||
from cx_core.action_type.predefined_action_type import PredefinedActionType
|
||||
from cx_core.action_type.scene_action_type import SceneActionType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cx_core import Controller
|
||||
|
||||
ActionsMapping = dict[ActionEvent, list[ActionType]]
|
||||
|
||||
action_type_mapping: dict[str, type[ActionType]] = {
|
||||
"action": PredefinedActionType,
|
||||
"service": CallServiceActionType,
|
||||
"scene": SceneActionType,
|
||||
"delay": DelayActionType,
|
||||
}
|
||||
|
||||
|
||||
def parse_actions(controller: "Controller", data: CustomActions) -> list[ActionType]:
|
||||
actions: CustomActions
|
||||
if isinstance(data, (list, tuple)):
|
||||
actions = list(data)
|
||||
else:
|
||||
actions = [data]
|
||||
|
||||
return [_parse_action(controller, action) for action in actions]
|
||||
|
||||
|
||||
def _parse_action(controller: "Controller", action: CustomAction) -> ActionType:
|
||||
if isinstance(action, str):
|
||||
return PredefinedActionType(controller, {"action": action})
|
||||
try:
|
||||
return next(
|
||||
action_type(controller, action)
|
||||
for key in action
|
||||
for action_type_key, action_type in action_type_mapping.items()
|
||||
if key == action_type_key
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(
|
||||
f"Not able to parse `{action}`. Available keys are: {list(action_type_mapping.keys())}"
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from cx_core.integration import EventData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cx_core import Controller
|
||||
|
||||
|
||||
class ActionType(ABC):
|
||||
controller: "Controller"
|
||||
|
||||
def __init__(self, controller: "Controller", action: dict[str, Any]) -> None:
|
||||
self.controller = controller
|
||||
self.initialize(**action)
|
||||
|
||||
def initialize(self, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def run(self, extra: EventData | None = None) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.__class__.__name__}"
|
||||
@@ -0,0 +1,52 @@
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from cx_core.action_type.base import ActionType
|
||||
from cx_core.integration import EventData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cx_core.type_controller import Entity, TypeController
|
||||
|
||||
|
||||
class CallServiceActionType(ActionType):
|
||||
service: str
|
||||
# Priority order for entity_id:
|
||||
# - Inside data
|
||||
# - In the same level as "service"
|
||||
# - From the main config if the domain matches
|
||||
entity_id: str | None
|
||||
data: dict[str, Any]
|
||||
|
||||
def initialize(self, **kwargs: Any) -> None:
|
||||
self.service = kwargs["service"]
|
||||
self.data = kwargs.get("data", {})
|
||||
|
||||
self.entity_id = self.data.get("entity_id") or kwargs.get("entity_id")
|
||||
if (
|
||||
self.entity_id is None
|
||||
and self._check_controller_isinstance_type_controller()
|
||||
):
|
||||
type_controller = cast("TypeController[Entity]", self.controller)
|
||||
if self._get_service_domain(self.service) in type_controller.domains:
|
||||
self.entity_id = type_controller.entity.name
|
||||
if "entity_id" in self.data:
|
||||
del self.data["entity_id"]
|
||||
|
||||
def _check_controller_isinstance_type_controller(self) -> bool:
|
||||
# This is checked dynamically without the isinstance to avoid
|
||||
# circular dependency
|
||||
class_names = [c.__name__ for c in type(self.controller).mro()]
|
||||
return "TypeController" in class_names
|
||||
|
||||
def _get_service_domain(self, service: str) -> str:
|
||||
return service.replace(".", "/").split("/")[0]
|
||||
|
||||
async def run(self, extra: EventData | None = None) -> None:
|
||||
if self.entity_id:
|
||||
await self.controller.call_service(
|
||||
self.service, entity_id=self.entity_id, **self.data
|
||||
)
|
||||
else:
|
||||
await self.controller.call_service(self.service, **self.data)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Service ({self.service})"
|
||||
@@ -0,0 +1,17 @@
|
||||
from typing import Any
|
||||
|
||||
from cx_core.action_type.base import ActionType
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class DelayActionType(ActionType):
|
||||
delay: int
|
||||
|
||||
def initialize(self, **kwargs: Any) -> None:
|
||||
self.delay = kwargs["delay"]
|
||||
|
||||
async def run(self, extra: EventData | None = None) -> None:
|
||||
await self.controller.sleep(self.delay)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Delay ({self.delay} seconds)"
|
||||
@@ -0,0 +1,121 @@
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from cx_const import (
|
||||
ActionFunction,
|
||||
ActionFunctionWithParams,
|
||||
ActionParams,
|
||||
PredefinedActionsMapping,
|
||||
TypeAction,
|
||||
)
|
||||
from cx_core.action_type.base import ActionType
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
def _get_action(action_value: TypeAction) -> ActionFunctionWithParams:
|
||||
if isinstance(action_value, tuple):
|
||||
return action_value
|
||||
else:
|
||||
return (action_value, tuple())
|
||||
|
||||
|
||||
def _get_arguments(
|
||||
action: ActionFunction,
|
||||
args: ActionParams,
|
||||
predefined_action_kwargs: dict[str, Any],
|
||||
extra: EventData | None,
|
||||
) -> tuple[ActionParams, dict[str, Any]]:
|
||||
action_parameters = inspect.signature(action).parameters
|
||||
action_parameters_without_extra = {
|
||||
key: param for key, param in action_parameters.items() if key != "extra"
|
||||
}
|
||||
action_parameters_without_default = {
|
||||
key: param
|
||||
for key, param in action_parameters.items()
|
||||
if param.default is inspect.Signature.empty
|
||||
}
|
||||
action_args: dict[str, Any] = dict(
|
||||
zip(action_parameters_without_extra.keys(), args)
|
||||
) # ControllerX args
|
||||
action_positional_args = set(action_args.keys())
|
||||
action_args.update(predefined_action_kwargs) # User args
|
||||
action_args.update({"extra": extra} if "extra" in action_parameters else {})
|
||||
action_args = {
|
||||
key: value for key, value in action_args.items() if key in action_parameters
|
||||
}
|
||||
|
||||
if len(set(action_parameters_without_default).difference(action_args)) != 0:
|
||||
error_msg = [
|
||||
f"`{action.__name__}` action is missing some parameters. Parameters available:"
|
||||
]
|
||||
for key, param in action_parameters_without_extra.items():
|
||||
if hasattr(param.annotation, "__name__"):
|
||||
attr_msg = f" {key}: {param.annotation.__name__}"
|
||||
else:
|
||||
attr_msg = f" {key}:"
|
||||
if param.default is not inspect.Signature.empty:
|
||||
attr_msg += f" [default: {param.default}]"
|
||||
if key in action_args:
|
||||
attr_msg += f" (value given: {action_args[key]})"
|
||||
elif param.default is inspect.Signature.empty:
|
||||
attr_msg += " (missing)"
|
||||
error_msg.append(attr_msg)
|
||||
raise ValueError("\n".join(error_msg))
|
||||
|
||||
positional = tuple(
|
||||
value for key, value in action_args.items() if key in action_positional_args
|
||||
)
|
||||
action_args = {
|
||||
key: value
|
||||
for key, value in action_args.items()
|
||||
if key not in action_positional_args
|
||||
}
|
||||
return positional, action_args
|
||||
|
||||
|
||||
class PredefinedActionType(ActionType):
|
||||
predefined_action_key: str
|
||||
predefined_action_kwargs: dict[str, Any]
|
||||
predefined_actions_mapping: PredefinedActionsMapping
|
||||
|
||||
def _raise_action_key_not_found(
|
||||
self, predefined_action_key: str, predefined_actions: PredefinedActionsMapping
|
||||
) -> None:
|
||||
raise ValueError(
|
||||
f"`{predefined_action_key}` is not one of the predefined actions. "
|
||||
f"Available actions are: {list(predefined_actions.keys())}."
|
||||
"See more in: https://xaviml.github.io/controllerx/advanced/custom-controllers"
|
||||
)
|
||||
|
||||
def initialize(self, **kwargs: Any) -> None:
|
||||
self.predefined_action_key = kwargs.pop("action")
|
||||
self.predefined_action_kwargs = kwargs
|
||||
self.predefined_actions_mapping = (
|
||||
self.controller.get_predefined_actions_mapping()
|
||||
)
|
||||
if not self.predefined_actions_mapping:
|
||||
raise ValueError(
|
||||
f"Cannot use predefined actions for `{self.controller.__class__.__name__}` class."
|
||||
)
|
||||
if (
|
||||
not self.controller.contains_templating(self.predefined_action_key)
|
||||
and self.predefined_action_key not in self.predefined_actions_mapping
|
||||
):
|
||||
self._raise_action_key_not_found(
|
||||
self.predefined_action_key, self.predefined_actions_mapping
|
||||
)
|
||||
|
||||
async def run(self, extra: EventData | None = None) -> None:
|
||||
action_key = await self.controller.render_value(self.predefined_action_key)
|
||||
if action_key not in self.predefined_actions_mapping:
|
||||
self._raise_action_key_not_found(
|
||||
action_key, self.predefined_actions_mapping
|
||||
)
|
||||
action, args = _get_action(self.predefined_actions_mapping[action_key])
|
||||
positional, action_args = _get_arguments(
|
||||
action, args, self.predefined_action_kwargs, extra
|
||||
)
|
||||
await action(*positional, **action_args)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Predefined ({self.predefined_action_key})"
|
||||
@@ -0,0 +1,17 @@
|
||||
from typing import Any
|
||||
|
||||
from cx_core.action_type.base import ActionType
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class SceneActionType(ActionType):
|
||||
scene: str
|
||||
|
||||
def initialize(self, **kwargs: Any) -> None:
|
||||
self.scene = kwargs["scene"]
|
||||
|
||||
async def run(self, extra: EventData | None = None) -> None:
|
||||
await self.controller.call_service("scene/turn_on", entity_id=self.scene)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Scene ({self.scene})"
|
||||
@@ -0,0 +1,81 @@
|
||||
Color = tuple[float, float]
|
||||
Colors = list[Color]
|
||||
|
||||
# These are the 24 colors that appear in the circle color of home assistant
|
||||
default_color_wheel = [
|
||||
(0.701, 0.299),
|
||||
(0.667, 0.284),
|
||||
(0.581, 0.245),
|
||||
(0.477, 0.196),
|
||||
(0.385, 0.155),
|
||||
(0.301, 0.116),
|
||||
(0.217, 0.077),
|
||||
(0.157, 0.05),
|
||||
(0.136, 0.04),
|
||||
(0.137, 0.065),
|
||||
(0.141, 0.137),
|
||||
(0.146, 0.238),
|
||||
(0.323, 0.329), # 12; white color middle
|
||||
(0.151, 0.343),
|
||||
(0.157, 0.457),
|
||||
(0.164, 0.591),
|
||||
(0.17, 0.703),
|
||||
(0.172, 0.747),
|
||||
(0.199, 0.724),
|
||||
(0.269, 0.665),
|
||||
(0.36, 0.588),
|
||||
(0.444, 0.517),
|
||||
(0.527, 0.447),
|
||||
(0.612, 0.374),
|
||||
(0.677, 0.319),
|
||||
]
|
||||
|
||||
# These are the xy colors translated from color temperature (2000K to 6488K)
|
||||
# They were extracted from here https://www.waveformlighting.com/files/blackBodyLocus_1.txt
|
||||
color_temp_wheel = [
|
||||
(0.527, 0.413),
|
||||
(0.507, 0.415),
|
||||
(0.489, 0.415),
|
||||
(0.472, 0.413),
|
||||
(0.456, 0.41),
|
||||
(0.442, 0.406),
|
||||
(0.428, 0.401),
|
||||
(0.416, 0.396),
|
||||
(0.406, 0.391),
|
||||
(0.396, 0.386),
|
||||
(0.386, 0.38),
|
||||
(0.378, 0.375),
|
||||
(0.37, 0.37),
|
||||
(0.363, 0.365),
|
||||
(0.357, 0.361),
|
||||
(0.351, 0.358),
|
||||
(0.345, 0.355),
|
||||
(0.34, 0.352),
|
||||
(0.336, 0.349),
|
||||
(0.331, 0.346),
|
||||
(0.327, 0.343),
|
||||
(0.323, 0.339),
|
||||
(0.32, 0.336),
|
||||
(0.317, 0.333),
|
||||
(0.314, 0.33),
|
||||
]
|
||||
|
||||
COLOR_WHEELS = {
|
||||
"default_color_wheel": default_color_wheel,
|
||||
"color_temp_wheel": color_temp_wheel,
|
||||
}
|
||||
|
||||
|
||||
def get_color_wheel(colors: str | Colors) -> Colors:
|
||||
if isinstance(colors, str):
|
||||
if colors not in COLOR_WHEELS:
|
||||
raise ValueError(
|
||||
f"`{colors}` is not an option for `color_wheel`. Options are: {list(COLOR_WHEELS.keys())}"
|
||||
)
|
||||
return COLOR_WHEELS[colors]
|
||||
elif isinstance(colors, (list, tuple)):
|
||||
return colors
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Type {type(colors)} is not supported for `color_wheel` attribute"
|
||||
)
|
||||
@@ -0,0 +1,611 @@
|
||||
import asyncio
|
||||
import re
|
||||
import time
|
||||
from ast import literal_eval
|
||||
from asyncio import CancelledError, Task
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Awaitable, Callable
|
||||
from functools import wraps
|
||||
from typing import (
|
||||
Any,
|
||||
DefaultDict,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeVar,
|
||||
overload,
|
||||
)
|
||||
|
||||
import appdaemon.utils as utils
|
||||
import cx_core.integration as integration_module
|
||||
import cx_version
|
||||
from appdaemon.adapi import ADAPI
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from appdaemon.plugins.mqtt.mqttapi import Mqtt
|
||||
from cx_const import (
|
||||
ActionEvent,
|
||||
ActionFunction,
|
||||
CustomActionsMapping,
|
||||
DefaultActionsMapping,
|
||||
PredefinedActionsMapping,
|
||||
)
|
||||
from cx_core.action_type import ActionsMapping, parse_actions
|
||||
from cx_core.action_type.base import ActionType
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
DEFAULT_ACTION_DELTA = 300 # In milliseconds
|
||||
DEFAULT_MULTIPLE_CLICK_DELAY = 500 # In milliseconds
|
||||
MULTIPLE_CLICK_TOKEN = "$"
|
||||
|
||||
MODE_SINGLE = "single"
|
||||
MODE_RESTART = "restart"
|
||||
MODE_QUEUED = "queued"
|
||||
MODE_PARALLEL = "parallel"
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def action(method: Callable[..., Awaitable[Any]]) -> ActionFunction:
|
||||
@wraps(method)
|
||||
async def _action_impl(controller: "Controller", *args: Any, **kwargs: Any) -> None:
|
||||
continue_call = await controller.before_action(method.__name__, *args, **kwargs)
|
||||
if continue_call:
|
||||
await method(controller, *args, **kwargs)
|
||||
|
||||
return _action_impl
|
||||
|
||||
|
||||
def run_in(fn: Callable[..., Any], delay: float, **kwargs: Any) -> "Task[None]":
|
||||
"""
|
||||
It runs the function (fn) in running event loop in `delay` seconds.
|
||||
This function has been created because the default run_in function
|
||||
from AppDaemon does not accept microseconds.
|
||||
"""
|
||||
|
||||
async def inner() -> None:
|
||||
await asyncio.sleep(delay)
|
||||
await fn(kwargs)
|
||||
|
||||
task = asyncio.create_task(inner())
|
||||
return task
|
||||
|
||||
|
||||
class Controller(Hass, Mqtt): # type: ignore[misc]
|
||||
"""
|
||||
This is the parent Controller, all controllers must extend from this class.
|
||||
"""
|
||||
|
||||
args: dict[str, Any]
|
||||
integration: Integration
|
||||
actions_mapping: ActionsMapping
|
||||
action_handles: DefaultDict[ActionEvent, Optional["Task[None]"]]
|
||||
action_delay_handles: dict[ActionEvent, str | None]
|
||||
multiple_click_actions: set[ActionEvent]
|
||||
action_delay: dict[ActionEvent, int]
|
||||
action_delta: dict[ActionEvent, int]
|
||||
action_times: dict[str, float]
|
||||
previous_states: dict[ActionEvent, str | None]
|
||||
multiple_click_action_times: dict[str, float]
|
||||
click_counter: Counter[ActionEvent]
|
||||
multiple_click_action_delay_tasks: DefaultDict[ActionEvent, Optional["Task[None]"]]
|
||||
multiple_click_delay: int
|
||||
|
||||
async def initialize(self) -> None:
|
||||
self.log(f"🎮 ControllerX {cx_version.__version__}", ascii_encode=False)
|
||||
await self.init()
|
||||
|
||||
async def init(self) -> None:
|
||||
controllers_ids: list[str] = self.get_list(self.args["controller"])
|
||||
self.integration = self.get_integration(self.args["integration"])
|
||||
|
||||
if "mapping" in self.args and "merge_mapping" in self.args:
|
||||
raise ValueError("`mapping` and `merge_mapping` cannot be used together")
|
||||
|
||||
custom_mapping: CustomActionsMapping | None = self.args.get("mapping", None)
|
||||
merge_mapping: CustomActionsMapping | None = self.args.get(
|
||||
"merge_mapping", None
|
||||
)
|
||||
|
||||
if custom_mapping is None:
|
||||
default_actions_mapping = self.get_default_actions_mapping(self.integration)
|
||||
self.actions_mapping = self.parse_action_mapping(default_actions_mapping) # type: ignore[arg-type]
|
||||
else:
|
||||
self.actions_mapping = self.parse_action_mapping(custom_mapping)
|
||||
|
||||
if merge_mapping is not None:
|
||||
self.actions_mapping.update(self.parse_action_mapping(merge_mapping))
|
||||
|
||||
# Filter actions with include and exclude
|
||||
if "actions" in self.args and "excluded_actions" in self.args:
|
||||
raise ValueError("`actions` and `excluded_actions` cannot be used together")
|
||||
include: list[ActionEvent] = self.get_list(
|
||||
self.args.get("actions", list(self.actions_mapping.keys()))
|
||||
)
|
||||
exclude: list[ActionEvent] = self.get_list(
|
||||
self.args.get("excluded_actions", [])
|
||||
)
|
||||
self.actions_mapping = self.filter_actions(
|
||||
self.actions_mapping, set(include), set(exclude)
|
||||
)
|
||||
|
||||
# Action delay
|
||||
self.action_delay = self.get_mapping_per_action(
|
||||
self.actions_mapping,
|
||||
custom=self.args.get("action_delay"),
|
||||
default=0,
|
||||
)
|
||||
self.action_delay_handles = defaultdict(lambda: None)
|
||||
self.action_handles = defaultdict(lambda: None)
|
||||
|
||||
# Action delta
|
||||
self.action_delta = self.get_mapping_per_action(
|
||||
self.actions_mapping,
|
||||
custom=self.args.get("action_delta"),
|
||||
default=DEFAULT_ACTION_DELTA,
|
||||
)
|
||||
self.action_times = defaultdict(float)
|
||||
|
||||
# Previous state
|
||||
self.previous_states = self.get_mapping_per_action(
|
||||
self.actions_mapping,
|
||||
custom=self.args.get("previous_state"),
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Multiple click
|
||||
self.multiple_click_actions = self.get_multiple_click_actions(
|
||||
self.actions_mapping
|
||||
)
|
||||
self.multiple_click_delay = self.args.get(
|
||||
"multiple_click_delay", DEFAULT_MULTIPLE_CLICK_DELAY
|
||||
)
|
||||
self.multiple_click_action_times = defaultdict(float)
|
||||
self.click_counter = Counter()
|
||||
self.multiple_click_action_delay_tasks = defaultdict(lambda: None)
|
||||
|
||||
# Mode
|
||||
self.mode = self.get_mapping_per_action(
|
||||
self.actions_mapping, custom=self.args.get("mode"), default=MODE_SINGLE
|
||||
)
|
||||
|
||||
# Listen for device changes
|
||||
for controller_id in controllers_ids:
|
||||
await self.integration.listen_changes(controller_id)
|
||||
|
||||
def filter_actions(
|
||||
self,
|
||||
actions_mapping: ActionsMapping,
|
||||
include: set[ActionEvent],
|
||||
exclude: set[ActionEvent],
|
||||
) -> ActionsMapping:
|
||||
allowed_actions = include - exclude
|
||||
return {
|
||||
key: value
|
||||
for key, value in actions_mapping.items()
|
||||
if key in allowed_actions
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_option(value: str, options: list[str], ctx: str | None = None) -> str:
|
||||
if value in options:
|
||||
return value
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{f'{ctx} - ' if ctx is not None else ''}`{value}` is not an option. "
|
||||
f"The options are {options}"
|
||||
)
|
||||
|
||||
def parse_integration(
|
||||
self, integration: str | dict[str, Any] | Any
|
||||
) -> dict[str, str]:
|
||||
if isinstance(integration, str):
|
||||
return {"name": integration}
|
||||
elif isinstance(integration, dict):
|
||||
if "name" in integration:
|
||||
return integration
|
||||
else:
|
||||
raise ValueError("'name' attribute is mandatory")
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Type {type(integration)} is not supported for `integration` attribute"
|
||||
)
|
||||
|
||||
def get_integration(self, integration: str | dict[str, Any]) -> Integration:
|
||||
parsed_integration = self.parse_integration(integration)
|
||||
kwargs = {k: v for k, v in parsed_integration.items() if k != "name"}
|
||||
integrations = integration_module.get_integrations(self, kwargs)
|
||||
integration_argument = self.get_option(
|
||||
parsed_integration["name"], [i.name for i in integrations]
|
||||
)
|
||||
return next(i for i in integrations if i.name == integration_argument)
|
||||
|
||||
def get_default_actions_mapping(
|
||||
self, integration: Integration
|
||||
) -> DefaultActionsMapping:
|
||||
actions_mapping = integration.get_default_actions_mapping()
|
||||
if actions_mapping is None:
|
||||
raise ValueError(
|
||||
f"This controller does not support {integration.name}. Use `mapping` to define the actions."
|
||||
)
|
||||
return actions_mapping
|
||||
|
||||
@overload
|
||||
def get_list(self, entities: list[T]) -> list[T]:
|
||||
pass
|
||||
|
||||
@overload
|
||||
def get_list(self, entities: T) -> list[T]:
|
||||
pass
|
||||
|
||||
def get_list(self, entities: list[T] | T) -> list[T]:
|
||||
if isinstance(entities, (list, tuple)):
|
||||
return list(entities)
|
||||
return [entities]
|
||||
|
||||
@overload
|
||||
def get_mapping_per_action(
|
||||
self,
|
||||
actions_mapping: ActionsMapping,
|
||||
*,
|
||||
custom: T | dict[ActionEvent, T] | None,
|
||||
default: None,
|
||||
) -> dict[ActionEvent, T | None]:
|
||||
pass
|
||||
|
||||
@overload
|
||||
def get_mapping_per_action(
|
||||
self,
|
||||
actions_mapping: ActionsMapping,
|
||||
*,
|
||||
custom: T | dict[ActionEvent, T] | None,
|
||||
default: T,
|
||||
) -> dict[ActionEvent, T]:
|
||||
pass
|
||||
|
||||
def get_mapping_per_action(
|
||||
self,
|
||||
actions_mapping: ActionsMapping,
|
||||
*,
|
||||
custom: T | dict[ActionEvent, T] | None,
|
||||
default: None | T,
|
||||
) -> dict[ActionEvent, T | None] | dict[ActionEvent, T]:
|
||||
if custom is not None and not isinstance(custom, dict):
|
||||
default = custom
|
||||
mapping = {action: default for action in actions_mapping}
|
||||
if custom is not None and isinstance(custom, dict):
|
||||
mapping.update(custom)
|
||||
return mapping
|
||||
|
||||
def parse_action_mapping(self, mapping: CustomActionsMapping) -> ActionsMapping:
|
||||
return {
|
||||
event: parse_actions(self, action)
|
||||
for event, action in mapping.items()
|
||||
if action is not None
|
||||
}
|
||||
|
||||
def get_multiple_click_actions(self, mapping: ActionsMapping) -> set[ActionEvent]:
|
||||
to_return: set[ActionEvent] = set()
|
||||
for key in mapping.keys():
|
||||
if not isinstance(key, str) or MULTIPLE_CLICK_TOKEN not in key:
|
||||
continue
|
||||
splitted = key.split(MULTIPLE_CLICK_TOKEN)
|
||||
assert 1 <= len(splitted) <= 2
|
||||
action_key, _ = splitted
|
||||
try:
|
||||
to_return.add(int(action_key))
|
||||
except ValueError:
|
||||
to_return.add(action_key)
|
||||
return to_return
|
||||
|
||||
def format_multiple_click_action(
|
||||
self, action_key: ActionEvent, click_count: int
|
||||
) -> str:
|
||||
return (
|
||||
str(action_key) + MULTIPLE_CLICK_TOKEN + str(click_count)
|
||||
) # e.g. toggle$2
|
||||
|
||||
async def _render_template(self, template: str) -> Any:
|
||||
result = await self.render_template(template)
|
||||
if result is None:
|
||||
raise ValueError(f"Template {template} returned None")
|
||||
try:
|
||||
return literal_eval(result)
|
||||
except (SyntaxError, ValueError):
|
||||
return result
|
||||
|
||||
_TEMPLATE_RE = re.compile(r"\s*\{\{.*\}\}")
|
||||
|
||||
def contains_templating(self, template: str) -> bool:
|
||||
is_template = self._TEMPLATE_RE.search(template) is not None
|
||||
if not is_template:
|
||||
self.log(f"`{template}` is not recognized as a template", level="DEBUG")
|
||||
return is_template
|
||||
|
||||
async def render_value(self, value: Any) -> Any:
|
||||
if isinstance(value, str) and self.contains_templating(value):
|
||||
return await self._render_template(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
async def render_attributes(self, attributes: dict[str, Any]) -> dict[str, Any]:
|
||||
new_attributes: dict[str, Any] = {}
|
||||
for key, value in attributes.items():
|
||||
new_value = await self.render_value(value)
|
||||
if isinstance(value, dict):
|
||||
new_value = await self.render_attributes(value)
|
||||
new_attributes[key] = new_value
|
||||
return new_attributes
|
||||
|
||||
async def call_service(
|
||||
self, service: str, render_template: bool = True, **attributes: Any
|
||||
) -> Any | None:
|
||||
service = service.replace(".", "/")
|
||||
to_log = ["\n", f"🤖 Service: \033[1m{service.replace('/', '.')}\033[0m"]
|
||||
if service != "template/render" and render_template:
|
||||
attributes = await self.render_attributes(attributes)
|
||||
for attribute, value in attributes.items():
|
||||
if isinstance(value, float):
|
||||
value = f"{value:.2f}"
|
||||
to_log.append(f" - {attribute}: {value}")
|
||||
self.log("\n".join(to_log), level="INFO", ascii_encode=False)
|
||||
return await ADAPI.call_service(self, service, **attributes)
|
||||
|
||||
@utils.sync_decorator # type: ignore[untyped-decorator]
|
||||
async def get_state(
|
||||
self,
|
||||
entity_id: str | None = None,
|
||||
attribute: str | Literal["all"] | None = None,
|
||||
default: Any | None = None,
|
||||
namespace: str | None = None,
|
||||
copy: bool = True,
|
||||
**kwargs: dict[str, Any], # left in intentionally for compatibility
|
||||
) -> Any | dict[str, Any] | None:
|
||||
rendered_entity_id = await self.render_value(entity_id)
|
||||
return await super().get_state(
|
||||
rendered_entity_id, attribute, default=default, copy=copy
|
||||
)
|
||||
|
||||
async def handle_action(
|
||||
self,
|
||||
action_key: str,
|
||||
previous_state: str | None = None,
|
||||
extra: EventData | None = None,
|
||||
) -> None:
|
||||
if (
|
||||
action_key in self.actions_mapping
|
||||
and self.previous_states[action_key] is not None
|
||||
and previous_state != self.previous_states[action_key]
|
||||
):
|
||||
self.log(
|
||||
f"🎮 `{action_key}` not triggered because previous action was `{previous_state}`",
|
||||
level="DEBUG",
|
||||
ascii_encode=False,
|
||||
)
|
||||
return
|
||||
if (
|
||||
action_key in self.actions_mapping
|
||||
and action_key not in self.multiple_click_actions
|
||||
):
|
||||
previous_call_time = self.action_times[action_key]
|
||||
now = time.time() * 1000
|
||||
self.action_times[action_key] = now
|
||||
if now - previous_call_time > self.action_delta[action_key]:
|
||||
await self.call_action(action_key, extra=extra)
|
||||
elif action_key in self.multiple_click_actions:
|
||||
now = time.time() * 1000
|
||||
previous_call_time = self.multiple_click_action_times.get(action_key, now)
|
||||
self.multiple_click_action_times[action_key] = now
|
||||
if now - previous_call_time > self.multiple_click_delay:
|
||||
pass
|
||||
|
||||
previous_task = self.multiple_click_action_delay_tasks[action_key]
|
||||
if previous_task is not None:
|
||||
previous_task.cancel()
|
||||
|
||||
self.click_counter[action_key] += 1
|
||||
click_count = self.click_counter[action_key]
|
||||
|
||||
new_task = run_in(
|
||||
self.multiple_click_call_action,
|
||||
self.multiple_click_delay / 1000,
|
||||
action_key=action_key,
|
||||
extra=extra,
|
||||
click_count=click_count,
|
||||
)
|
||||
self.multiple_click_action_delay_tasks[action_key] = new_task
|
||||
else:
|
||||
self.log(
|
||||
f"🎮 Button event triggered, but not registered: `{action_key}`",
|
||||
level="DEBUG",
|
||||
ascii_encode=False,
|
||||
)
|
||||
|
||||
async def multiple_click_call_action(self, kwargs: dict[str, Any]) -> None:
|
||||
action_key: ActionEvent = kwargs["action_key"]
|
||||
extra: EventData = kwargs["extra"]
|
||||
click_count: int = kwargs["click_count"]
|
||||
self.multiple_click_action_delay_tasks[action_key] = None
|
||||
self.log(
|
||||
f"🎮 {action_key} clicked `{click_count}` time(s)",
|
||||
level="DEBUG",
|
||||
ascii_encode=False,
|
||||
)
|
||||
self.click_counter[action_key] = 0
|
||||
click_action_key = self.format_multiple_click_action(action_key, click_count)
|
||||
if click_action_key in self.actions_mapping:
|
||||
await self.call_action(click_action_key, extra=extra)
|
||||
elif action_key in self.actions_mapping and click_count == 1:
|
||||
await self.call_action(action_key, extra=extra)
|
||||
|
||||
async def call_action(
|
||||
self, action_key: ActionEvent, extra: EventData | None = None
|
||||
) -> None:
|
||||
self.log(
|
||||
f"🎮 Button event triggered: `{action_key}`",
|
||||
level="INFO",
|
||||
ascii_encode=False,
|
||||
)
|
||||
self.log(
|
||||
f"Extra:\n{extra}",
|
||||
level="DEBUG",
|
||||
)
|
||||
delay = self.action_delay[action_key]
|
||||
if delay > 0:
|
||||
handle = self.action_delay_handles[action_key]
|
||||
if handle is not None:
|
||||
await self.cancel_timer(handle)
|
||||
self.log(
|
||||
f"🕒 Running action(s) from `{action_key}` in {delay} seconds",
|
||||
level="INFO",
|
||||
ascii_encode=False,
|
||||
)
|
||||
new_handle = await self.run_in(
|
||||
self.action_timer_callback, delay, action_key=action_key, extra=extra
|
||||
)
|
||||
self.action_delay_handles[action_key] = new_handle
|
||||
else:
|
||||
await self.action_timer_callback({"action_key": action_key, "extra": extra})
|
||||
|
||||
async def _apply_mode_strategy(self, action_key: ActionEvent) -> bool:
|
||||
previous_task = self.action_handles[action_key]
|
||||
if previous_task is None or previous_task.done():
|
||||
return False
|
||||
if self.mode[action_key] == MODE_SINGLE:
|
||||
self.log(
|
||||
f"There is already an action executing for `{action_key}`. "
|
||||
"If you want a different behaviour change `mode` parameter, "
|
||||
"the default value is `single`.",
|
||||
level="WARNING",
|
||||
)
|
||||
return True
|
||||
elif self.mode[action_key] == MODE_RESTART:
|
||||
previous_task.cancel()
|
||||
elif self.mode[action_key] == MODE_QUEUED:
|
||||
await previous_task
|
||||
elif self.mode[action_key] == MODE_PARALLEL:
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
f"`{self.mode[action_key]}` is not a possible value for `mode` parameter."
|
||||
"Possible values: `single`, `restart`, `queued` and `parallel`."
|
||||
)
|
||||
return False
|
||||
|
||||
async def action_timer_callback(self, kwargs: dict[str, Any]) -> None:
|
||||
action_key: ActionEvent = kwargs["action_key"]
|
||||
extra: EventData = kwargs["extra"]
|
||||
self.action_delay_handles[action_key] = None
|
||||
skip = await self._apply_mode_strategy(action_key)
|
||||
if skip:
|
||||
return
|
||||
action_types = self.actions_mapping[action_key]
|
||||
task = asyncio.create_task(self.call_action_types(action_types, extra))
|
||||
self.action_handles[action_key] = task
|
||||
try:
|
||||
await task
|
||||
except CancelledError:
|
||||
self.log(
|
||||
f"Task(s) from `{action_key}` was/were canceled and executed again",
|
||||
level="DEBUG",
|
||||
)
|
||||
|
||||
async def call_action_types(
|
||||
self, action_types: list[ActionType], extra: EventData | None = None
|
||||
) -> None:
|
||||
for action_type in action_types:
|
||||
self.log(
|
||||
f"🏃 Running `{action_type}` now",
|
||||
level="INFO",
|
||||
ascii_encode=False,
|
||||
)
|
||||
await action_type.run(extra=extra)
|
||||
|
||||
async def before_action(self, action: str, *args: str, **kwargs: Any) -> bool:
|
||||
"""
|
||||
Controllers have the option to implement this function, which is called
|
||||
everytime before an action is called and it has the check_before_action decorator.
|
||||
It should return True if the action shoul be called.
|
||||
Otherwise it should return False.
|
||||
"""
|
||||
return True
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
"""
|
||||
Controllers can implement this function. It should return a dict
|
||||
with the states that a controller can take and the functions as values.
|
||||
This is used for zigbee2mqtt support.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
"""
|
||||
Controllers can implement this function. It should return a dict
|
||||
with the event id that a controller can take and the functions as values.
|
||||
This is used for deCONZ support.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
"""
|
||||
Controllers can implement this function. It should return a dict
|
||||
with the command that a controller can take and the functions as values.
|
||||
This is used for ZHA support.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str | None:
|
||||
"""
|
||||
This method can be override for controllers that do not support
|
||||
the standard extraction of the actions on cx_core/integration/zha.py
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
"""
|
||||
Controllers can implement this function. It should return a dict
|
||||
with the command that a controller can take and the functions as values.
|
||||
This is used for Lutron support.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_state_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
"""
|
||||
Controllers can implement this function. It should return a dict
|
||||
with the command that a controller can take and the functions as values.
|
||||
This is used for State integration support.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_homematic_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
"""
|
||||
Controllers can implement this function. It should return a dict
|
||||
with the command that a controller can take and the functions as values.
|
||||
This is used for Homematic support.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_shelly_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
"""
|
||||
Controllers can implement this function. It should return a dict
|
||||
with the command that a controller can take and the functions as values.
|
||||
This is used for Shelly support.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_shellyforhass_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
"""
|
||||
Controllers can implement this function. It should return a dict
|
||||
with the command that a controller can take and the functions as values.
|
||||
This is used for Shelly for HASS support.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_tasmota_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
"""
|
||||
Controllers can implement this function. It should return a dict
|
||||
with the command that a controller can take and the functions as values.
|
||||
This is used for Tasmota support.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
|
||||
return {}
|
||||
@@ -0,0 +1,40 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cx_core.type_controller import Entity, TypeController
|
||||
|
||||
|
||||
class FeatureSupport:
|
||||
controller: "TypeController[Entity]"
|
||||
update_supported_features: bool
|
||||
_supported_features: int | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller: "TypeController[Entity]",
|
||||
supported_features: int | None = None,
|
||||
update_supported_features: bool = False,
|
||||
) -> None:
|
||||
self.controller = controller
|
||||
self._supported_features = supported_features
|
||||
self.update_supported_features = update_supported_features
|
||||
|
||||
@property
|
||||
async def supported_features(self) -> int:
|
||||
if self._supported_features is None or self.update_supported_features:
|
||||
bitfield: str = await self.controller.get_entity_state(
|
||||
attribute="supported_features"
|
||||
)
|
||||
if bitfield is not None:
|
||||
self._supported_features = int(bitfield)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"`supported_features` could not be read from `{self.controller.entity}`. Entity might not be available."
|
||||
)
|
||||
return self._supported_features
|
||||
|
||||
async def is_supported(self, feature: int) -> bool:
|
||||
return feature & await self.supported_features != 0
|
||||
|
||||
async def not_supported(self, feature: int) -> bool:
|
||||
return not await self.is_supported(feature)
|
||||
@@ -0,0 +1,9 @@
|
||||
class CoverSupport:
|
||||
OPEN = 1
|
||||
CLOSE = 2
|
||||
SET_COVER_POSITION = 4
|
||||
STOP = 8
|
||||
OPEN_TILT = 16
|
||||
CLOSE_TILT = 32
|
||||
STOP_TILT = 64
|
||||
SET_TILT_POSITION = 128
|
||||
@@ -0,0 +1,4 @@
|
||||
class LightSupport:
|
||||
EFFECT = 4
|
||||
FLASH = 8
|
||||
TRANSITION = 32
|
||||
@@ -0,0 +1,20 @@
|
||||
class MediaPlayerSupport:
|
||||
PAUSE = 1
|
||||
SEEK = 2
|
||||
VOLUME_SET = 4
|
||||
VOLUME_MUTE = 8
|
||||
PREVIOUS_TRACK = 16
|
||||
NEXT_TRACK = 32
|
||||
TURN_ON = 128
|
||||
TURN_OFF = 256
|
||||
PLAY_MEDIA = 512
|
||||
VOLUME_STEP = 1024
|
||||
SELECT_SOURCE = 2048
|
||||
STOP = 4096
|
||||
CLEAR_PLAYLIST = 8192
|
||||
PLAY = 16384
|
||||
SHUFFLE_SET = 32768
|
||||
SELECT_SOUND_MODE = 65536
|
||||
SUPPORT_BROWSE_MEDIA = 131072
|
||||
SUPPORT_REPEAT_SET = 262144
|
||||
SUPPORT_GROUPING = 524288
|
||||
@@ -0,0 +1,35 @@
|
||||
import abc
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_helper import get_classes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cx_core.controller import Controller
|
||||
|
||||
EventData = dict[str, Any]
|
||||
|
||||
|
||||
class Integration(abc.ABC):
|
||||
name: str
|
||||
controller: "Controller"
|
||||
kwargs: dict[str, Any]
|
||||
|
||||
def __init__(self, controller: "Controller", kwargs: dict[str, Any]):
|
||||
self.controller = controller
|
||||
self.kwargs = kwargs
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return None
|
||||
|
||||
@abc.abstractmethod
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def get_integrations(
|
||||
controller: "Controller", kwargs: dict[str, Any]
|
||||
) -> list[Integration]:
|
||||
integration_classes = get_classes(__file__, __package__, Integration)
|
||||
integrations = [cls_(controller, kwargs) for cls_ in integration_classes]
|
||||
return integrations
|
||||
@@ -0,0 +1,34 @@
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
LISTENS_TO_ID = "id"
|
||||
LISTENS_TO_UNIQUE_ID = "unique_id"
|
||||
|
||||
|
||||
class DeCONZIntegration(Integration):
|
||||
name = "deconz"
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_deconz_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
listens_to = self.kwargs.get("listen_to", LISTENS_TO_ID)
|
||||
if listens_to not in (LISTENS_TO_ID, LISTENS_TO_UNIQUE_ID):
|
||||
raise ValueError(
|
||||
"`listens_to` for deCONZ integration should either be `id` or `unique_id`"
|
||||
)
|
||||
await Hass.listen_event(
|
||||
self.controller,
|
||||
self.event_callback,
|
||||
"deconz_event",
|
||||
**{listens_to: controller_id}
|
||||
)
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
type_ = self.kwargs.get("type", "event")
|
||||
await self.controller.handle_action(data[type_], extra=data)
|
||||
@@ -0,0 +1,41 @@
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
|
||||
class EventIntegration(Integration):
|
||||
name = "event"
|
||||
|
||||
def get_arg(self, arg: str) -> Any:
|
||||
try:
|
||||
return self.kwargs[arg]
|
||||
except KeyError:
|
||||
raise ValueError(f"{arg} is a mandatory field for event integration.")
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
event_type: str = self.get_arg("event_type")
|
||||
controller_key: str = self.get_arg("controller_key")
|
||||
self.controller.log(
|
||||
f"Listening to `{event_type}` events for controller `{controller_key}={controller_id}`"
|
||||
)
|
||||
await Hass.listen_event(
|
||||
self.controller,
|
||||
self.event_callback,
|
||||
event_type,
|
||||
**{controller_key: controller_id},
|
||||
)
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
action_template: str = self.get_arg("action_template")
|
||||
try:
|
||||
action = action_template.format(**data)
|
||||
except Exception:
|
||||
self.controller.log(
|
||||
f"Template `{action_template}` could not be rendered with data={data}",
|
||||
level="WARNING",
|
||||
)
|
||||
return
|
||||
await self.controller.handle_action(action)
|
||||
@@ -0,0 +1,36 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cx_core.controller import Controller
|
||||
|
||||
|
||||
class HomematicIntegration(Integration):
|
||||
name = "homematic"
|
||||
_registered_controller_ids: set[str]
|
||||
|
||||
def __init__(self, controller: "Controller", kwargs: dict[str, Any]):
|
||||
self._registered_controller_ids = set()
|
||||
super().__init__(controller, kwargs)
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_homematic_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
self._registered_controller_ids.add(controller_id)
|
||||
await Hass.listen_event(
|
||||
self.controller, self.event_callback, "homematic.keypress"
|
||||
)
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
if data["name"] not in self._registered_controller_ids:
|
||||
return
|
||||
param = data["param"]
|
||||
channel = data["channel"]
|
||||
action = f"{param}_{channel}"
|
||||
await self.controller.handle_action(action, extra=data)
|
||||
@@ -0,0 +1,28 @@
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
|
||||
class LutronIntegration(Integration):
|
||||
name = "lutron_caseta"
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_lutron_caseta_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
await Hass.listen_event(
|
||||
self.controller,
|
||||
self.event_callback,
|
||||
"lutron_caseta_button_event",
|
||||
serial=controller_id,
|
||||
)
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
button = data["button_number"]
|
||||
action_type = data["action"]
|
||||
action = f"button_{button}_{action_type}"
|
||||
await self.controller.handle_action(action, extra=data)
|
||||
@@ -0,0 +1,43 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.mqtt.mqttapi import Mqtt
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
|
||||
class MQTTIntegration(Integration):
|
||||
name = "mqtt"
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_z2m_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
await Mqtt.listen_event(
|
||||
self.controller, self.event_callback, topic=controller_id, namespace="mqtt"
|
||||
)
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
self.controller.log(f"MQTT data event: {data}", level="DEBUG")
|
||||
payload_key = self.kwargs.get("key")
|
||||
if "payload" not in data:
|
||||
return
|
||||
payload = data["payload"]
|
||||
action_key: str
|
||||
if payload_key is None:
|
||||
action_key = payload
|
||||
else:
|
||||
try:
|
||||
action_key = str(json.loads(payload)[payload_key]).lower()
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValueError(
|
||||
f"`key` is being used ({payload_key}). "
|
||||
f"Following payload is not a valid JSON: {payload}"
|
||||
)
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"Following payload does not contain `{payload_key}`: {payload}"
|
||||
)
|
||||
await self.controller.handle_action(action_key)
|
||||
@@ -0,0 +1,25 @@
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
|
||||
class ShellyIntegration(Integration):
|
||||
name = "shelly"
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_shelly_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
await Hass.listen_event(
|
||||
self.controller, self.event_callback, "shelly.click", device=controller_id
|
||||
)
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
click_type = data["click_type"]
|
||||
channel = data["channel"]
|
||||
action = f"{click_type}_{channel}"
|
||||
await self.controller.handle_action(action, extra=data)
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
|
||||
class ShellyForHASSIntegration(Integration):
|
||||
name = "shellyforhass"
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_shellyforhass_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
await Hass.listen_event(
|
||||
self.controller,
|
||||
self.event_callback,
|
||||
"shellyforhass.click",
|
||||
entity_id=controller_id,
|
||||
)
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
click_type = data["click_type"]
|
||||
action = f"{click_type}"
|
||||
await self.controller.handle_action(action, extra=data)
|
||||
@@ -0,0 +1,28 @@
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import Integration
|
||||
|
||||
|
||||
class StateIntegration(Integration):
|
||||
name = "state"
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_state_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
attribute = self.kwargs.get("attribute", None)
|
||||
await Hass.listen_state(
|
||||
self.controller, self.state_callback, controller_id, attribute=attribute
|
||||
)
|
||||
|
||||
async def state_callback(
|
||||
self,
|
||||
entity: str | None,
|
||||
attribute: str | None,
|
||||
old: str | None,
|
||||
new: str,
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
await self.controller.handle_action(new, previous_state=old)
|
||||
@@ -0,0 +1,51 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.mqtt.mqttapi import Mqtt
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
|
||||
class TasmotaIntegration(Integration):
|
||||
name = "tasmota"
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_tasmota_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
component_key = self.kwargs.get("component")
|
||||
if component_key is None:
|
||||
raise ValueError(
|
||||
"`component` attribute is mandatory. "
|
||||
"Check example from https://xaviml.github.io/controllerx/start/integrations/tasmota"
|
||||
)
|
||||
await Mqtt.listen_event(
|
||||
self.controller, self.event_callback, topic=controller_id, namespace="mqtt"
|
||||
)
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
self.controller.log(f"MQTT data event: {data}", level="DEBUG")
|
||||
component_key: str = self.kwargs["component"]
|
||||
payload_key: str = self.kwargs.get("key", "Action")
|
||||
if "payload" not in data:
|
||||
return
|
||||
payload: str = data["payload"]
|
||||
# Even though this checks if "compoenent_key" is in the payload
|
||||
# (as string, not dictionary), it is preferred for its performance
|
||||
if component_key not in payload:
|
||||
return
|
||||
try:
|
||||
action_key = str(json.loads(payload)[component_key][payload_key])
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValueError(
|
||||
f"`key` is being used ({payload_key}). "
|
||||
f"Following payload is not a valid JSON: {payload}"
|
||||
)
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
"Following payload does not contain "
|
||||
f"payload_key=`{payload_key}`: {payload}"
|
||||
)
|
||||
await self.controller.handle_action(action_key)
|
||||
@@ -0,0 +1,85 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from appdaemon.plugins.mqtt.mqttapi import Mqtt
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
LISTENS_TO_HA = "ha"
|
||||
LISTENS_TO_MQTT = "mqtt"
|
||||
LISTENS_TO_EVENT = "event"
|
||||
|
||||
|
||||
class Z2MIntegration(Integration):
|
||||
name = "z2m"
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_z2m_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
listens_to = self.kwargs.get("listen_to", LISTENS_TO_HA)
|
||||
if listens_to == LISTENS_TO_HA:
|
||||
self.controller.log(
|
||||
"⚠️ Listening to HA sensor actions is now deprecated and will be removed in the future. Use `listen_to: mqtt` or `listen_to: event` instead."
|
||||
" Read more about it here: https://xaviml.github.io/controllerx/others/z2m-ha-sensor-deprecated",
|
||||
level="WARNING",
|
||||
ascii_encode=False,
|
||||
)
|
||||
await Hass.listen_state(self.controller, self.state_callback, controller_id)
|
||||
elif listens_to == LISTENS_TO_MQTT:
|
||||
topic_prefix = self.kwargs.get("topic_prefix", "zigbee2mqtt")
|
||||
await Mqtt.listen_event(
|
||||
self.controller,
|
||||
self.event_callback,
|
||||
topic=f"{topic_prefix}/{controller_id}",
|
||||
namespace="mqtt",
|
||||
)
|
||||
elif listens_to == LISTENS_TO_EVENT:
|
||||
await Hass.listen_state(
|
||||
self.controller,
|
||||
self.state_callback,
|
||||
f"event.{controller_id}",
|
||||
attribute="event_type",
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"`listen_to` has to be either `ha`, `mqtt` or `event`. Default is `ha`."
|
||||
)
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
self.controller.log(f"MQTT data event: {data}", level="DEBUG")
|
||||
action_key = self.kwargs.get("action_key", "action")
|
||||
action_group_key = self.kwargs.get("action_group_key", "action_group")
|
||||
if "payload" not in data:
|
||||
return
|
||||
payload = json.loads(data["payload"])
|
||||
if action_key not in payload:
|
||||
self.controller.log(
|
||||
f"⚠️ There is no `{action_key}` in the MQTT topic payload",
|
||||
level="WARNING",
|
||||
ascii_encode=False,
|
||||
)
|
||||
return
|
||||
if action_group_key in payload and "action_group" in self.kwargs:
|
||||
action_group = self.controller.get_list(self.kwargs["action_group"])
|
||||
if payload["action_group"] not in action_group:
|
||||
self.controller.log(
|
||||
f"Action group {payload['action_group']} not found in "
|
||||
f"action groups: {action_group}",
|
||||
level="DEBUG",
|
||||
)
|
||||
return
|
||||
await self.controller.handle_action(payload[action_key], extra=payload)
|
||||
|
||||
async def state_callback(
|
||||
self,
|
||||
entity: str | None,
|
||||
attribute: str | None,
|
||||
old: str | None,
|
||||
new: str,
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
await self.controller.handle_action(new, previous_state=old)
|
||||
@@ -0,0 +1,46 @@
|
||||
from typing import Any
|
||||
|
||||
from appdaemon.plugins.hass.hassapi import Hass
|
||||
from cx_const import DefaultActionsMapping
|
||||
from cx_core.integration import EventData, Integration
|
||||
|
||||
|
||||
class ZHAIntegration(Integration):
|
||||
name = "zha"
|
||||
|
||||
def get_default_actions_mapping(self) -> DefaultActionsMapping | None:
|
||||
return self.controller.get_zha_actions_mapping()
|
||||
|
||||
async def listen_changes(self, controller_id: str) -> None:
|
||||
await Hass.listen_event(
|
||||
self.controller, self.event_callback, "zha_event", device_ieee=controller_id
|
||||
)
|
||||
|
||||
def get_action(self, data: EventData) -> str:
|
||||
command = data["command"]
|
||||
args = data["args"]
|
||||
if isinstance(args, dict):
|
||||
args = args["args"]
|
||||
args = list(map(str, args))
|
||||
action = command
|
||||
if not (command == "stop" or command == "release"):
|
||||
if len(args) > 0:
|
||||
action += "_" + "_".join(args)
|
||||
return action
|
||||
|
||||
async def event_callback(
|
||||
self, event_name: str, data: EventData, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
action = self.controller.get_zha_action(data)
|
||||
if action is None:
|
||||
# If there is no action extracted from the controller then
|
||||
# we extract with the standard function
|
||||
try:
|
||||
action = self.get_action(data)
|
||||
except Exception:
|
||||
self.controller.log(
|
||||
f"The following event could not be parsed: {data}", level="WARNING"
|
||||
)
|
||||
return
|
||||
|
||||
await self.controller.handle_action(action, extra=data)
|
||||
@@ -0,0 +1,69 @@
|
||||
import abc
|
||||
from typing import Any
|
||||
|
||||
from cx_core.controller import Controller, action
|
||||
|
||||
DEFAULT_DELAY = 350 # In milliseconds
|
||||
DEFAULT_RELEASE_DELAY = 0 # In seconds
|
||||
|
||||
|
||||
class ReleaseHoldController(Controller, abc.ABC):
|
||||
DEFAULT_MAX_LOOPS = 50
|
||||
|
||||
on_hold: bool
|
||||
delay: float
|
||||
max_loops: int
|
||||
hold_release_toggle: bool
|
||||
release_delay: int
|
||||
|
||||
async def init(self) -> None:
|
||||
self.on_hold = False
|
||||
self.delay = self.args.get("delay", self.default_delay())
|
||||
self.max_loops = self.args.get(
|
||||
"max_loops", ReleaseHoldController.DEFAULT_MAX_LOOPS
|
||||
)
|
||||
self.hold_release_toggle = self.args.get("hold_release_toggle", False)
|
||||
self.release_delay = self.args.get("release_delay", DEFAULT_RELEASE_DELAY)
|
||||
await super().init()
|
||||
|
||||
@action
|
||||
async def release(self) -> None:
|
||||
if self.release_delay > 0:
|
||||
await self.sleep(self.release_delay)
|
||||
self.on_hold = False
|
||||
|
||||
async def hold(self, *args: Any) -> None:
|
||||
loops = 0
|
||||
self.on_hold = True
|
||||
stop = False
|
||||
while self.on_hold and not stop:
|
||||
stop = await self.hold_loop(*args)
|
||||
# Stop the iteration if we either stop from the hold_loop
|
||||
# or we reached the max loop number
|
||||
stop = stop or loops >= self.max_loops
|
||||
await self.sleep(self.delay / 1000)
|
||||
loops += 1
|
||||
self.on_hold = False
|
||||
|
||||
async def before_action(self, action: str, *args: Any, **kwargs: Any) -> bool:
|
||||
super_before_action = await super().before_action(action, *args, **kwargs)
|
||||
to_return = not (action == "hold" and self.on_hold)
|
||||
if action == "hold" and self.on_hold and self.hold_release_toggle:
|
||||
self.on_hold = False
|
||||
return super_before_action and to_return
|
||||
|
||||
@abc.abstractmethod
|
||||
async def hold_loop(self, *args: Any) -> bool:
|
||||
"""
|
||||
This function is called by the ReleaseHoldController depending on the settings.
|
||||
It stops calling the function once release action is called or when this function
|
||||
returns True.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def default_delay(self) -> int:
|
||||
"""
|
||||
This function can be overwritten for each device to indeicate the delay
|
||||
for the specific device, by default it returns the default delay from the app
|
||||
"""
|
||||
return DEFAULT_DELAY
|
||||
@@ -0,0 +1,112 @@
|
||||
import abc
|
||||
from dataclasses import dataclass
|
||||
|
||||
from cx_const import Number, StepperDir
|
||||
|
||||
|
||||
class MinMax:
|
||||
def __init__(self, min: Number, max: Number, margin: float = 0.05) -> None:
|
||||
self._min = min
|
||||
self._max = max
|
||||
self.margin_dist = (max - min) * margin
|
||||
|
||||
@property
|
||||
def min(self) -> Number:
|
||||
return self._min
|
||||
|
||||
@property
|
||||
def max(self) -> Number:
|
||||
return self._max
|
||||
|
||||
def is_min(self, value: Number) -> bool:
|
||||
return self._min == value
|
||||
|
||||
def is_max(self, value: Number) -> bool:
|
||||
return self._max == value
|
||||
|
||||
def is_between(self, value: Number) -> bool:
|
||||
return self._min < value < self._max
|
||||
|
||||
def in_min_boundaries(self, value: Number) -> bool:
|
||||
return self._min <= value <= (self._min + self.margin_dist)
|
||||
|
||||
def in_max_boundaries(self, value: Number) -> bool:
|
||||
return (self._max - self.margin_dist) <= value <= self._max
|
||||
|
||||
def clip(self, value: Number) -> Number:
|
||||
return max(self._min, min(value, self._max))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"MinMax({self.min}, {self.max})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class StepperOutput:
|
||||
next_value: Number
|
||||
next_direction: str | None
|
||||
|
||||
@property
|
||||
def exceeded(self) -> bool:
|
||||
return self.next_direction is None
|
||||
|
||||
|
||||
class Stepper(abc.ABC):
|
||||
sign_mapping = {StepperDir.UP: 1, StepperDir.DOWN: -1}
|
||||
|
||||
min_max: MinMax
|
||||
steps: Number
|
||||
previous_direction: str
|
||||
relative_steps: bool
|
||||
|
||||
@staticmethod
|
||||
def invert_direction(direction: str) -> str:
|
||||
return StepperDir.UP if direction == StepperDir.DOWN else StepperDir.DOWN
|
||||
|
||||
@staticmethod
|
||||
def sign(direction: str) -> int:
|
||||
return Stepper.sign_mapping[direction]
|
||||
|
||||
@staticmethod
|
||||
def apply_sign(value: Number, direction: str) -> Number:
|
||||
return Stepper.sign(direction) * value
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_max: MinMax,
|
||||
steps: Number,
|
||||
previous_direction: str = StepperDir.DOWN,
|
||||
relative_steps: bool = True,
|
||||
) -> None:
|
||||
self.min_max = min_max
|
||||
self.steps = steps
|
||||
self.previous_direction = previous_direction
|
||||
self.relative_steps = relative_steps
|
||||
|
||||
def _compute_step(self) -> float:
|
||||
if self.relative_steps:
|
||||
max_ = self.min_max.max
|
||||
min_ = self.min_max.min
|
||||
return (max_ - min_) / self.steps
|
||||
else:
|
||||
return self.steps
|
||||
|
||||
def get_direction(self, value: Number, direction: str) -> str:
|
||||
if direction == StepperDir.TOGGLE:
|
||||
direction = Stepper.invert_direction(self.previous_direction)
|
||||
self.previous_direction = direction
|
||||
return direction
|
||||
|
||||
@abc.abstractmethod
|
||||
def step(self, value: Number, direction: str) -> StepperOutput:
|
||||
"""
|
||||
This function updates the value according to the steps
|
||||
that needs to take and returns the new value together with
|
||||
the new direction it will need to go. If next_direction is
|
||||
None, the loop will stop executing.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class InvertStepper(Stepper):
|
||||
def step(self, value: Number, direction: str) -> StepperOutput:
|
||||
return StepperOutput(self.apply_sign(value, direction), next_direction=None)
|
||||
@@ -0,0 +1,17 @@
|
||||
from cx_const import Number
|
||||
from cx_core.stepper import Stepper, StepperOutput
|
||||
|
||||
|
||||
class BounceStepper(Stepper):
|
||||
def step(self, value: Number, direction: str) -> StepperOutput:
|
||||
value = self.min_max.clip(value)
|
||||
step = self._compute_step()
|
||||
|
||||
new_value = value + Stepper.apply_sign(step, direction)
|
||||
if self.min_max.is_between(new_value):
|
||||
return StepperOutput(round(new_value, 3), next_direction=direction)
|
||||
else:
|
||||
new_value = 2 * self.min_max.clip(new_value) - new_value
|
||||
return StepperOutput(
|
||||
round(new_value, 3), next_direction=Stepper.invert_direction(direction)
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
from cx_const import Number, StepperDir
|
||||
from cx_core.stepper import MinMax, Stepper, StepperOutput
|
||||
|
||||
|
||||
class IndexLoopStepper(Stepper):
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
previous_direction: str = StepperDir.DOWN,
|
||||
relative_steps: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
MinMax(0, size - 1),
|
||||
size,
|
||||
previous_direction,
|
||||
relative_steps,
|
||||
)
|
||||
|
||||
def step(self, value: Number, direction: str) -> StepperOutput:
|
||||
value = self.min_max.clip(value)
|
||||
sign = self.sign(direction)
|
||||
# We add +1 to make the max be included
|
||||
max_ = int(self.min_max.max) + 1
|
||||
min_ = int(self.min_max.min)
|
||||
if self.relative_steps:
|
||||
step = (max_ - min_) // self.steps
|
||||
else:
|
||||
step = self.steps
|
||||
|
||||
new_value = (int(value) + step * sign) % (max_ - min_) + min_
|
||||
return StepperOutput(new_value, next_direction=direction)
|
||||
@@ -0,0 +1,16 @@
|
||||
from cx_const import Number
|
||||
from cx_core.stepper import Stepper, StepperOutput
|
||||
|
||||
|
||||
class LoopStepper(Stepper):
|
||||
def step(self, value: Number, direction: str) -> StepperOutput:
|
||||
value = self.min_max.clip(value)
|
||||
max_ = self.min_max.max
|
||||
min_ = self.min_max.min
|
||||
step = self._compute_step()
|
||||
|
||||
new_value = (
|
||||
((value + Stepper.apply_sign(step, direction)) - min_) % (max_ - min_)
|
||||
) + min_
|
||||
new_value = round(new_value, 3)
|
||||
return StepperOutput(new_value, next_direction=direction)
|
||||
@@ -0,0 +1,26 @@
|
||||
from cx_const import Number, StepperDir
|
||||
from cx_core.stepper import Stepper, StepperOutput
|
||||
|
||||
|
||||
class StopStepper(Stepper):
|
||||
def get_direction(self, value: Number, direction: str) -> str:
|
||||
value = self.min_max.clip(value)
|
||||
if direction == StepperDir.TOGGLE and self.min_max.in_min_boundaries(value):
|
||||
self.previous_direction = StepperDir.UP
|
||||
return self.previous_direction
|
||||
if direction == StepperDir.TOGGLE and self.min_max.in_max_boundaries(value):
|
||||
self.previous_direction = StepperDir.DOWN
|
||||
return self.previous_direction
|
||||
return super().get_direction(value, direction)
|
||||
|
||||
def step(self, value: Number, direction: str) -> StepperOutput:
|
||||
value = self.min_max.clip(value)
|
||||
step = self._compute_step()
|
||||
|
||||
new_value = value + Stepper.apply_sign(step, direction)
|
||||
new_value = round(new_value, 3)
|
||||
if self.min_max.is_between(new_value):
|
||||
return StepperOutput(new_value, next_direction=direction)
|
||||
else:
|
||||
new_value = self.min_max.clip(new_value)
|
||||
return StepperOutput(new_value, next_direction=None)
|
||||
@@ -0,0 +1,125 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from cx_const import Cover, PredefinedActionsMapping
|
||||
from cx_core.controller import action
|
||||
from cx_core.feature_support.cover import CoverSupport
|
||||
from cx_core.type_controller import Entity, TypeController
|
||||
|
||||
|
||||
class CoverController(TypeController[Entity]):
|
||||
"""
|
||||
This is the main class that controls the coveres for different devices.
|
||||
Type of actions:
|
||||
- Open
|
||||
- Close
|
||||
Parameters taken:
|
||||
- controller (required): Inherited from Controller
|
||||
- cover (required): cover entity name
|
||||
- open_position (optional): The open position. Default is 100
|
||||
- close_position (optional): The close position. Default is 0
|
||||
"""
|
||||
|
||||
domains = ["cover"]
|
||||
entity_arg = "cover"
|
||||
|
||||
open_position: int
|
||||
close_position: int
|
||||
|
||||
cover_duration: int | None
|
||||
|
||||
is_supposedly_moving: bool = False
|
||||
stop_timer_handle: str | None = None
|
||||
|
||||
async def init(self) -> None:
|
||||
self.open_position = self.args.get("open_position", 100)
|
||||
self.close_position = self.args.get("close_position", 0)
|
||||
self.cover_duration = self.args.get("cover_duration")
|
||||
if self.open_position < self.close_position:
|
||||
raise ValueError("`open_position` must be higher than `close_position`")
|
||||
await super().init()
|
||||
|
||||
def _get_entity_type(self) -> type[Entity]:
|
||||
return Entity
|
||||
|
||||
def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
|
||||
return {
|
||||
Cover.OPEN: self.open,
|
||||
Cover.CLOSE: self.close,
|
||||
Cover.STOP: self.stop,
|
||||
Cover.TOGGLE_OPEN: (self.toggle, (self.open,)),
|
||||
Cover.TOGGLE_CLOSE: (self.toggle, (self.close,)),
|
||||
}
|
||||
|
||||
async def cover_stopped_cb(self, kwargs: dict[str, Any]) -> None:
|
||||
self.is_supposedly_moving = False
|
||||
self.stop_timer_handle = None
|
||||
|
||||
async def start_timer(self) -> None:
|
||||
if self.cover_duration is None:
|
||||
return
|
||||
await self.stop_timer()
|
||||
self.is_supposedly_moving = True
|
||||
self.stop_timer_handle = await self.run_in(
|
||||
self.cover_stopped_cb, self.cover_duration
|
||||
)
|
||||
|
||||
async def stop_timer(self) -> None:
|
||||
if self.stop_timer_handle is not None:
|
||||
self.is_supposedly_moving = False
|
||||
await self.cancel_timer(self.stop_timer_handle)
|
||||
|
||||
@action
|
||||
async def open(self) -> None:
|
||||
if await self.feature_support.is_supported(CoverSupport.SET_COVER_POSITION):
|
||||
await self.call_service(
|
||||
"cover/set_cover_position",
|
||||
entity_id=self.entity.name,
|
||||
position=self.open_position,
|
||||
)
|
||||
elif await self.feature_support.is_supported(CoverSupport.OPEN):
|
||||
await self.call_service("cover/open_cover", entity_id=self.entity.name)
|
||||
else:
|
||||
self.log(
|
||||
f"⚠️ `{self.entity}` does not support SET_COVER_POSITION or OPEN",
|
||||
level="WARNING",
|
||||
ascii_encode=False,
|
||||
)
|
||||
return
|
||||
await self.start_timer()
|
||||
|
||||
@action
|
||||
async def close(self) -> None:
|
||||
if await self.feature_support.is_supported(CoverSupport.SET_COVER_POSITION):
|
||||
await self.call_service(
|
||||
"cover/set_cover_position",
|
||||
entity_id=self.entity.name,
|
||||
position=self.close_position,
|
||||
)
|
||||
elif await self.feature_support.is_supported(CoverSupport.CLOSE):
|
||||
await self.call_service("cover/close_cover", entity_id=self.entity.name)
|
||||
else:
|
||||
self.log(
|
||||
f"⚠️ `{self.entity}` does not support SET_COVER_POSITION or CLOSE",
|
||||
level="WARNING",
|
||||
ascii_encode=False,
|
||||
)
|
||||
return
|
||||
await self.start_timer()
|
||||
|
||||
@action
|
||||
async def stop(self) -> None:
|
||||
await self.stop_timer()
|
||||
await self.call_service("cover/stop_cover", entity_id=self.entity.name)
|
||||
|
||||
@action
|
||||
async def toggle(self, action: Callable[[], Awaitable[None]]) -> None:
|
||||
cover_state = await self.get_entity_state()
|
||||
if (
|
||||
cover_state == "opening"
|
||||
or cover_state == "closing"
|
||||
or self.is_supposedly_moving
|
||||
):
|
||||
await self.stop()
|
||||
else:
|
||||
await action()
|
||||
@@ -0,0 +1,958 @@
|
||||
import asyncio
|
||||
from functools import lru_cache
|
||||
from typing import Any, Literal
|
||||
|
||||
from cx_const import Light, Number, PredefinedActionsMapping, StepperDir, StepperMode
|
||||
from cx_core.color_helper import Color, get_color_wheel
|
||||
from cx_core.controller import action
|
||||
from cx_core.feature_support.light import LightSupport
|
||||
from cx_core.integration import EventData
|
||||
from cx_core.integration.deconz import DeCONZIntegration
|
||||
from cx_core.integration.z2m import Z2MIntegration
|
||||
from cx_core.integration.zha import ZHAIntegration
|
||||
from cx_core.release_hold_controller import ReleaseHoldController
|
||||
from cx_core.stepper import MinMax, Stepper
|
||||
from cx_core.stepper.bounce_stepper import BounceStepper
|
||||
from cx_core.stepper.index_loop_stepper import IndexLoopStepper
|
||||
from cx_core.stepper.loop_stepper import LoopStepper
|
||||
from cx_core.stepper.stop_stepper import StopStepper
|
||||
from cx_core.type_controller import Entity, TypeController
|
||||
|
||||
DEFAULT_MANUAL_STEPS = 10
|
||||
DEFAULT_AUTOMATIC_STEPS = 10
|
||||
DEFAULT_MIN_BRIGHTNESS = 1
|
||||
DEFAULT_MAX_BRIGHTNESS = 255
|
||||
DEFAULT_MIN_WHITE_VALUE = 1
|
||||
DEFAULT_MAX_WHITE_VALUE = 255
|
||||
DEFAULT_MIN_COLOR_TEMP = 153
|
||||
DEFAULT_MAX_COLOR_TEMP = 500
|
||||
DEFAULT_COLOR_TEMP = (
|
||||
DEFAULT_MAX_COLOR_TEMP - DEFAULT_MIN_COLOR_TEMP
|
||||
) // 2 + DEFAULT_MIN_COLOR_TEMP
|
||||
DEFAULT_TRANSITION = 300
|
||||
DEFAULT_ADD_TRANSITION = True
|
||||
DEFAULT_TRANSITION_TURN_TOGGLE = False
|
||||
DEFAULT_HOLD_TOGGLE_DIRECTION_INIT = "up"
|
||||
|
||||
ColorMode = Literal["auto", "xy_color", "color_temp"]
|
||||
|
||||
COLOR_MODES = {"hs", "xy", "rgb", "rgbw", "rgbww"}
|
||||
STEPPER_MODES: dict[str, type[Stepper]] = {
|
||||
StepperMode.STOP: StopStepper,
|
||||
StepperMode.LOOP: LoopStepper,
|
||||
StepperMode.BOUNCE: BounceStepper,
|
||||
}
|
||||
|
||||
|
||||
class LightEntity(Entity):
|
||||
color_mode: ColorMode
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
entities: list[str] | None = None,
|
||||
color_mode: ColorMode = "auto",
|
||||
) -> None:
|
||||
super().__init__(name, entities)
|
||||
self.color_mode = color_mode
|
||||
|
||||
|
||||
class LightController(TypeController[LightEntity], ReleaseHoldController):
|
||||
"""
|
||||
This is the main class that controls the lights for different devices.
|
||||
Type of actions:
|
||||
- On/Off/Toggle
|
||||
- Brightness click and hold
|
||||
- Color temperature click and hold
|
||||
- xy color click and hold
|
||||
If a light supports xy_color and color_temperature, then xy_color will be the
|
||||
default functionality. Parameters taken:
|
||||
- controller (required): Inherited from Controller
|
||||
- light (required): This is either the light entity name or a dictionary as
|
||||
{name: string, color_mode: auto | xy_color | color_temp}
|
||||
- delay (optional): Inherited from ReleaseHoldController
|
||||
- manual_steps (optional): Number of steps to go from min to max when clicking.
|
||||
- automatic_steps (optional): Number of steps to go from min to max when smoothing.
|
||||
"""
|
||||
|
||||
ATTRIBUTE_BRIGHTNESS = "brightness"
|
||||
ATTRIBUTE_WHITE_VALUE = "white_value"
|
||||
# With the following attribute, it will select color_temp or xy_color, depending on the light.
|
||||
ATTRIBUTE_COLOR = "color"
|
||||
ATTRIBUTE_COLOR_TEMP = "color_temp"
|
||||
ATTRIBUTE_XY_COLOR = "xy_color"
|
||||
|
||||
ATTRIBUTES_LIST = [
|
||||
ATTRIBUTE_BRIGHTNESS,
|
||||
ATTRIBUTE_WHITE_VALUE,
|
||||
ATTRIBUTE_COLOR,
|
||||
ATTRIBUTE_COLOR_TEMP,
|
||||
ATTRIBUTE_XY_COLOR,
|
||||
]
|
||||
|
||||
index_color = 0
|
||||
value_attribute = None
|
||||
|
||||
# These are intermediate variables to store the checked value
|
||||
smooth_power_on_check: bool
|
||||
remove_transition_check: bool
|
||||
next_direction: str | None = None
|
||||
|
||||
manual_steps: Number
|
||||
automatic_steps: Number
|
||||
min_max_attributes: dict[str, MinMax]
|
||||
|
||||
domains = ["light"]
|
||||
entity_arg = "light"
|
||||
|
||||
_supported_color_modes: set[str] | None
|
||||
|
||||
async def init(self) -> None:
|
||||
self.manual_steps = self.args.get("manual_steps", DEFAULT_MANUAL_STEPS)
|
||||
self.automatic_steps = self.args.get("automatic_steps", DEFAULT_AUTOMATIC_STEPS)
|
||||
|
||||
self.min_max_attributes = {
|
||||
self.ATTRIBUTE_BRIGHTNESS: MinMax(
|
||||
self.args.get("min_brightness", DEFAULT_MIN_BRIGHTNESS),
|
||||
self.args.get("max_brightness", DEFAULT_MAX_BRIGHTNESS),
|
||||
),
|
||||
self.ATTRIBUTE_WHITE_VALUE: MinMax(
|
||||
self.args.get("min_white_value", DEFAULT_MIN_WHITE_VALUE),
|
||||
self.args.get("max_white_value", DEFAULT_MAX_WHITE_VALUE),
|
||||
),
|
||||
self.ATTRIBUTE_COLOR_TEMP: MinMax(
|
||||
self.args.get("min_color_temp", DEFAULT_MIN_COLOR_TEMP),
|
||||
self.args.get("max_color_temp", DEFAULT_MAX_COLOR_TEMP),
|
||||
),
|
||||
}
|
||||
|
||||
self.transition = self.args.get("transition", DEFAULT_TRANSITION)
|
||||
self.color_wheel = get_color_wheel(
|
||||
self.args.get("color_wheel", "default_color_wheel")
|
||||
)
|
||||
self._supported_color_modes = self.args.get("supported_color_modes")
|
||||
|
||||
self.smooth_power_on = self.args.get(
|
||||
"smooth_power_on", self.supports_smooth_power_on()
|
||||
)
|
||||
self.add_transition = self.args.get("add_transition", DEFAULT_ADD_TRANSITION)
|
||||
self.add_transition_turn_toggle = self.args.get(
|
||||
"add_transition_turn_toggle", DEFAULT_TRANSITION_TURN_TOGGLE
|
||||
)
|
||||
self.hold_toggle_direction_init = self.get_option(
|
||||
self.args.get(
|
||||
"hold_toggle_direction_init", DEFAULT_HOLD_TOGGLE_DIRECTION_INIT
|
||||
),
|
||||
[StepperDir.UP, StepperDir.DOWN],
|
||||
"`hold_toggle_direction_init`",
|
||||
)
|
||||
await super().init()
|
||||
|
||||
def _get_entity_type(self) -> type[LightEntity]:
|
||||
return LightEntity
|
||||
|
||||
def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
|
||||
return {
|
||||
Light.ON: self.on,
|
||||
Light.OFF: self.off,
|
||||
Light.TOGGLE: self.toggle,
|
||||
Light.TOGGLE_FULL_BRIGHTNESS: (
|
||||
self.toggle_full,
|
||||
(LightController.ATTRIBUTE_BRIGHTNESS,),
|
||||
),
|
||||
Light.TOGGLE_FULL_WHITE_VALUE: (
|
||||
self.toggle_full,
|
||||
(LightController.ATTRIBUTE_WHITE_VALUE,),
|
||||
),
|
||||
Light.TOGGLE_FULL_COLOR_TEMP: (
|
||||
self.toggle_full,
|
||||
(LightController.ATTRIBUTE_COLOR_TEMP,),
|
||||
),
|
||||
Light.TOGGLE_MIN_BRIGHTNESS: (
|
||||
self.toggle_min,
|
||||
(LightController.ATTRIBUTE_BRIGHTNESS,),
|
||||
),
|
||||
Light.TOGGLE_MIN_WHITE_VALUE: (
|
||||
self.toggle_min,
|
||||
(LightController.ATTRIBUTE_WHITE_VALUE,),
|
||||
),
|
||||
Light.TOGGLE_MIN_COLOR_TEMP: (
|
||||
self.toggle_min,
|
||||
(LightController.ATTRIBUTE_COLOR_TEMP,),
|
||||
),
|
||||
Light.RELEASE: self.release,
|
||||
Light.ON_FULL_BRIGHTNESS: (
|
||||
self.on_full,
|
||||
(LightController.ATTRIBUTE_BRIGHTNESS,),
|
||||
),
|
||||
Light.ON_FULL_WHITE_VALUE: (
|
||||
self.on_full,
|
||||
(LightController.ATTRIBUTE_WHITE_VALUE,),
|
||||
),
|
||||
Light.ON_FULL_COLOR_TEMP: (
|
||||
self.on_full,
|
||||
(LightController.ATTRIBUTE_COLOR_TEMP,),
|
||||
),
|
||||
Light.ON_MIN_BRIGHTNESS: (
|
||||
self.on_min,
|
||||
(LightController.ATTRIBUTE_BRIGHTNESS,),
|
||||
),
|
||||
Light.ON_MIN_WHITE_VALUE: (
|
||||
self.on_min,
|
||||
(LightController.ATTRIBUTE_WHITE_VALUE,),
|
||||
),
|
||||
Light.ON_MIN_COLOR_TEMP: (
|
||||
self.on_min,
|
||||
(LightController.ATTRIBUTE_COLOR_TEMP,),
|
||||
),
|
||||
Light.ON_MIN_MAX_BRIGHTNESS: (
|
||||
self.on_min_max,
|
||||
(LightController.ATTRIBUTE_BRIGHTNESS,),
|
||||
),
|
||||
Light.ON_MAX_MIN_BRIGHTNESS: (
|
||||
self.on_max_min,
|
||||
(LightController.ATTRIBUTE_BRIGHTNESS,),
|
||||
),
|
||||
Light.ON_MIN_MAX_COLOR_TEMP: (
|
||||
self.on_min_max,
|
||||
(LightController.ATTRIBUTE_COLOR_TEMP,),
|
||||
),
|
||||
Light.ON_MAX_MIN_COLOR_TEMP: (
|
||||
self.on_max_min,
|
||||
(LightController.ATTRIBUTE_COLOR_TEMP,),
|
||||
),
|
||||
Light.SET_HALF_BRIGHTNESS: (
|
||||
self.set_value,
|
||||
(
|
||||
LightController.ATTRIBUTE_BRIGHTNESS,
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
Light.SET_HALF_WHITE_VALUE: (
|
||||
self.set_value,
|
||||
(
|
||||
LightController.ATTRIBUTE_WHITE_VALUE,
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
Light.SET_HALF_COLOR_TEMP: (
|
||||
self.set_value,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR_TEMP,
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
Light.SYNC: self.sync,
|
||||
Light.CLICK: self.click,
|
||||
Light.CLICK_BRIGHTNESS_UP: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.CLICK_BRIGHTNESS_DOWN: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.CLICK_WHITE_VALUE_UP: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_WHITE_VALUE,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.CLICK_WHITE_VALUE_DOWN: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_WHITE_VALUE,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.CLICK_COLOR_UP: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.CLICK_COLOR_DOWN: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.CLICK_COLOR_TEMP_UP: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.CLICK_COLOR_TEMP_DOWN: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.CLICK_XY_COLOR_UP: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_XY_COLOR,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.CLICK_XY_COLOR_DOWN: (
|
||||
self.click,
|
||||
(
|
||||
LightController.ATTRIBUTE_XY_COLOR,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.HOLD: self.hold,
|
||||
Light.HOLD_BRIGHTNESS_UP: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.HOLD_BRIGHTNESS_DOWN: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.HOLD_BRIGHTNESS_TOGGLE: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.TOGGLE,
|
||||
),
|
||||
),
|
||||
Light.HOLD_WHITE_VALUE_UP: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_WHITE_VALUE,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.HOLD_WHITE_VALUE_DOWN: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_WHITE_VALUE,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.HOLD_WHITE_VALUE_TOGGLE: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_WHITE_VALUE,
|
||||
StepperDir.TOGGLE,
|
||||
),
|
||||
),
|
||||
Light.HOLD_COLOR_UP: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.HOLD_COLOR_DOWN: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.HOLD_COLOR_TOGGLE: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR,
|
||||
StepperDir.TOGGLE,
|
||||
),
|
||||
),
|
||||
Light.HOLD_COLOR_TEMP_UP: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.HOLD_COLOR_TEMP_DOWN: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.HOLD_COLOR_TEMP_TOGGLE: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.TOGGLE,
|
||||
),
|
||||
),
|
||||
Light.HOLD_XY_COLOR_UP: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_XY_COLOR,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Light.HOLD_XY_COLOR_DOWN: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_XY_COLOR,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Light.HOLD_XY_COLOR_TOGGLE: (
|
||||
self.hold,
|
||||
(
|
||||
LightController.ATTRIBUTE_XY_COLOR,
|
||||
StepperDir.TOGGLE,
|
||||
),
|
||||
),
|
||||
Light.XYCOLOR_FROM_CONTROLLER: self.xycolor_from_controller,
|
||||
Light.COLORTEMP_FROM_CONTROLLER: self.colortemp_from_controller,
|
||||
Light.COLORTEMP_FROM_CONTROLLER_STEP: (
|
||||
self.attribute_from_controller_step,
|
||||
(LightController.ATTRIBUTE_COLOR_TEMP,),
|
||||
),
|
||||
Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL: self.brightness_from_controller_level,
|
||||
Light.BRIGHTNESS_FROM_CONTROLLER_ANGLE: self.brightness_from_controller_angle,
|
||||
Light.BRIGHTNESS_FROM_CONTROLLER_STEP: (
|
||||
self.attribute_from_controller_step,
|
||||
(LightController.ATTRIBUTE_BRIGHTNESS,),
|
||||
),
|
||||
}
|
||||
|
||||
async def check_remove_transition(self, on_from_user: bool) -> bool:
|
||||
return (
|
||||
not self.add_transition
|
||||
or (on_from_user and not self.add_transition_turn_toggle)
|
||||
or await self.feature_support.not_supported(LightSupport.TRANSITION)
|
||||
)
|
||||
|
||||
async def call_light_service(
|
||||
self, service: str, force_transition: bool = False, **attributes: Any
|
||||
) -> None:
|
||||
if "transition" not in attributes:
|
||||
attributes["transition"] = self.transition / 1000
|
||||
if self.remove_transition_check and not force_transition:
|
||||
del attributes["transition"]
|
||||
await self.call_service(service, entity_id=self.entity.name, **attributes)
|
||||
|
||||
async def _on(self, **attributes: Any) -> None:
|
||||
await self.call_light_service("light/turn_on", **attributes)
|
||||
|
||||
@action
|
||||
async def on(self, attributes: dict[str, float] | None = None) -> None:
|
||||
attributes = {} if attributes is None else attributes
|
||||
await self._on(**attributes)
|
||||
|
||||
async def _off(self, **attributes: Any) -> None:
|
||||
await self.call_light_service("light/turn_off", **attributes)
|
||||
|
||||
@action
|
||||
async def off(self) -> None:
|
||||
await self._off()
|
||||
|
||||
async def _toggle(self, **attributes: Any) -> None:
|
||||
await self.call_light_service("light/toggle", **attributes)
|
||||
|
||||
@action
|
||||
async def toggle(self, attributes: dict[str, float] | None = None) -> None:
|
||||
attributes = {} if attributes is None else attributes
|
||||
await self._toggle(**attributes)
|
||||
|
||||
async def _set_value(self, attribute: str, fraction: float) -> None:
|
||||
fraction = max(0, min(fraction, 1))
|
||||
min_ = self.min_max_attributes[attribute].min
|
||||
max_ = self.min_max_attributes[attribute].max
|
||||
value = (max_ - min_) * fraction + min_
|
||||
await self._on(**{attribute: value})
|
||||
|
||||
@action
|
||||
async def set_value(self, attribute: str, fraction: float) -> None:
|
||||
await self._set_value(attribute, fraction)
|
||||
|
||||
@action
|
||||
async def toggle_full(self, attribute: str) -> None:
|
||||
await self._toggle(**{attribute: self.min_max_attributes[attribute].max})
|
||||
|
||||
@action
|
||||
async def toggle_min(self, attribute: str) -> None:
|
||||
await self._toggle(**{attribute: self.min_max_attributes[attribute].min})
|
||||
|
||||
async def _on_full(self, attribute: str) -> None:
|
||||
await self._set_value(attribute, 1)
|
||||
|
||||
@action
|
||||
async def on_full(self, attribute: str) -> None:
|
||||
await self._on_full(attribute)
|
||||
|
||||
async def _on_min(self, attribute: str) -> None:
|
||||
await self._set_value(attribute, 0)
|
||||
|
||||
@action
|
||||
async def on_min(self, attribute: str) -> None:
|
||||
await self._on_min(attribute)
|
||||
|
||||
@action
|
||||
async def on_min_max(self, attribute: str) -> None:
|
||||
min_ = self.min_max_attributes[attribute].min
|
||||
max_ = self.min_max_attributes[attribute].max
|
||||
await self._on_min_max(attribute, default=min_, other=max_)
|
||||
|
||||
@action
|
||||
async def on_max_min(self, attribute: str) -> None:
|
||||
min_ = self.min_max_attributes[attribute].min
|
||||
max_ = self.min_max_attributes[attribute].max
|
||||
await self._on_min_max(attribute, default=max_, other=min_)
|
||||
|
||||
async def _on_min_max(
|
||||
self, attribute: str, *, default: Number, other: Number
|
||||
) -> None:
|
||||
light_state: str
|
||||
attribute_value: Number
|
||||
light_state, attribute_value = await asyncio.gather(
|
||||
self.get_entity_state(), self.get_entity_state(attribute=attribute)
|
||||
)
|
||||
|
||||
if light_state == "off" or attribute_value != default:
|
||||
await self._on(**{attribute: default})
|
||||
else:
|
||||
await self._on(**{attribute: other})
|
||||
|
||||
@action
|
||||
async def sync(
|
||||
self,
|
||||
brightness: int | None = None,
|
||||
color_temp: int = 370, # 2700K light
|
||||
xy_color: Color = (0.323, 0.329), # white colour
|
||||
) -> None:
|
||||
attributes: dict[Any, Any] = {}
|
||||
try:
|
||||
color_attribute = await self.get_attribute(LightController.ATTRIBUTE_COLOR)
|
||||
if color_attribute == LightController.ATTRIBUTE_COLOR_TEMP:
|
||||
attributes[color_attribute] = color_temp
|
||||
else:
|
||||
attributes[color_attribute] = list(xy_color)
|
||||
except ValueError:
|
||||
self.log(
|
||||
"⚠️ `sync` action will only change brightness",
|
||||
level="WARNING",
|
||||
ascii_encode=False,
|
||||
)
|
||||
await self._on(
|
||||
**attributes,
|
||||
brightness=(
|
||||
brightness
|
||||
or self.min_max_attributes[LightController.ATTRIBUTE_BRIGHTNESS].max
|
||||
),
|
||||
)
|
||||
|
||||
@action
|
||||
async def xycolor_from_controller(self, extra: EventData | None = None) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, Z2MIntegration):
|
||||
if "action_color" not in extra:
|
||||
self.log(
|
||||
"`action_color` is not present in the MQTT payload", level="WARNING"
|
||||
)
|
||||
return
|
||||
xy_color = extra["action_color"]
|
||||
await self._on(xy_color=[xy_color["x"], xy_color["y"]])
|
||||
elif isinstance(self.integration, DeCONZIntegration):
|
||||
if "xy" not in extra:
|
||||
self.log("`xy` is not present in the deCONZ event", level="WARNING")
|
||||
return
|
||||
await self._on(xy_color=list(extra["xy"]))
|
||||
|
||||
@action
|
||||
async def colortemp_from_controller(self, extra: EventData | None = None) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, Z2MIntegration):
|
||||
if "action_color_temperature" not in extra:
|
||||
self.log(
|
||||
"`action_color_temperature` is not present in the MQTT payload",
|
||||
level="WARNING",
|
||||
)
|
||||
return
|
||||
await self._on(color_temp=extra["action_color_temperature"])
|
||||
|
||||
@action
|
||||
async def brightness_from_controller_level(
|
||||
self, extra: EventData | None = None
|
||||
) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, Z2MIntegration):
|
||||
if "action_level" not in extra:
|
||||
self.log(
|
||||
"`action_level` is not present in the MQTT payload",
|
||||
level="WARNING",
|
||||
)
|
||||
return
|
||||
await self._on(brightness=extra["action_level"])
|
||||
|
||||
@action
|
||||
async def attribute_from_controller_step(
|
||||
self, attribute: str, extra: EventData | None = None
|
||||
) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, ZHAIntegration):
|
||||
try:
|
||||
step_mode: int = extra["params"]["step_mode"]
|
||||
step_size: int = extra["params"]["step_size"]
|
||||
stepper = self.generate_stepper(
|
||||
attribute, step_size, StepperMode.STOP, relative_steps=False
|
||||
)
|
||||
direction = StepperDir.UP
|
||||
if (
|
||||
attribute == LightController.ATTRIBUTE_BRIGHTNESS and step_mode == 1
|
||||
) or (
|
||||
attribute == LightController.ATTRIBUTE_COLOR_TEMP and step_mode == 3
|
||||
):
|
||||
direction = StepperDir.DOWN
|
||||
|
||||
# Transition time in seconds
|
||||
transition_time: int = extra["params"]["transition_time"]
|
||||
self.value_attribute = await self.get_value_attribute(attribute)
|
||||
extra_attributes = {
|
||||
"transition": transition_time,
|
||||
"force_transition": True,
|
||||
}
|
||||
await self.change_light_state(
|
||||
self.value_attribute,
|
||||
attribute,
|
||||
direction,
|
||||
stepper,
|
||||
extra_attributes=extra_attributes,
|
||||
)
|
||||
except (KeyError, TypeError):
|
||||
self.log(
|
||||
"`params` should be present in ZHA event with "
|
||||
"`step_mode`, `step_size`, and `transition_time`",
|
||||
level="WARNING",
|
||||
)
|
||||
|
||||
@action
|
||||
async def brightness_from_controller_angle(
|
||||
self,
|
||||
mode: str = StepperMode.STOP,
|
||||
steps: Number | None = None,
|
||||
extra: EventData | None = None,
|
||||
) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, Z2MIntegration):
|
||||
if "action_rotation_angle" not in extra:
|
||||
self.log(
|
||||
"`action_rotation_angle` is not present in the MQTT payload",
|
||||
level="WARNING",
|
||||
)
|
||||
return
|
||||
angle = extra["action_rotation_angle"]
|
||||
direction = StepperDir.UP if angle > 0 else StepperDir.DOWN
|
||||
await self._hold(
|
||||
LightController.ATTRIBUTE_BRIGHTNESS, direction, mode=mode, steps=steps
|
||||
)
|
||||
|
||||
@property
|
||||
async def supported_color_modes(self) -> set[str]:
|
||||
if self._supported_color_modes is None or self.update_supported_features:
|
||||
supported_color_modes: list[str] = await self.get_entity_state(
|
||||
attribute="supported_color_modes"
|
||||
)
|
||||
if supported_color_modes is not None:
|
||||
self._supported_color_modes = set(supported_color_modes)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"`supported_color_modes` could not be read from `{self.entity}`. "
|
||||
"Entity might not be available."
|
||||
)
|
||||
|
||||
return self._supported_color_modes
|
||||
|
||||
async def is_color_supported(self) -> bool:
|
||||
return len(COLOR_MODES.intersection(await self.supported_color_modes)) > 0
|
||||
|
||||
async def is_colortemp_supported(self) -> bool:
|
||||
return "color_temp" in await self.supported_color_modes
|
||||
|
||||
def generate_stepper(
|
||||
self, attribute: str, steps: Number, mode: str, *, relative_steps: bool = True
|
||||
) -> Stepper:
|
||||
previous_direction = Stepper.invert_direction(self.hold_toggle_direction_init)
|
||||
if attribute == LightController.ATTRIBUTE_XY_COLOR:
|
||||
return IndexLoopStepper(
|
||||
len(self.color_wheel),
|
||||
previous_direction,
|
||||
relative_steps,
|
||||
)
|
||||
if mode not in STEPPER_MODES:
|
||||
raise ValueError(
|
||||
f"`{mode}` mode is not available. Options are: {list(STEPPER_MODES.keys())}"
|
||||
)
|
||||
stepper_cls = STEPPER_MODES[mode]
|
||||
return stepper_cls(
|
||||
self.min_max_attributes[attribute],
|
||||
steps,
|
||||
previous_direction,
|
||||
relative_steps,
|
||||
)
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_stepper(
|
||||
self, attribute: str, steps: Number, mode: str, *, tag: str
|
||||
) -> Stepper:
|
||||
return self.generate_stepper(attribute, steps, mode)
|
||||
|
||||
async def get_attribute(self, attribute: str) -> str:
|
||||
if attribute == LightController.ATTRIBUTE_COLOR:
|
||||
if self.entity.color_mode == "auto":
|
||||
if await self.is_color_supported():
|
||||
return LightController.ATTRIBUTE_XY_COLOR
|
||||
elif await self.is_colortemp_supported():
|
||||
return LightController.ATTRIBUTE_COLOR_TEMP
|
||||
else:
|
||||
raise ValueError(
|
||||
"This light does not support xy_color or color_temp"
|
||||
)
|
||||
else:
|
||||
return self.entity.color_mode
|
||||
else:
|
||||
return attribute
|
||||
|
||||
async def get_value_attribute(self, attribute: str) -> Number:
|
||||
if self.smooth_power_on_check:
|
||||
return 0
|
||||
if attribute == LightController.ATTRIBUTE_XY_COLOR:
|
||||
return 0
|
||||
elif (
|
||||
attribute == LightController.ATTRIBUTE_BRIGHTNESS
|
||||
or attribute == LightController.ATTRIBUTE_WHITE_VALUE
|
||||
or attribute == LightController.ATTRIBUTE_COLOR_TEMP
|
||||
):
|
||||
value = await self.get_entity_state(attribute=attribute)
|
||||
if value is None and attribute == LightController.ATTRIBUTE_COLOR_TEMP:
|
||||
return float(DEFAULT_COLOR_TEMP)
|
||||
elif value is None and attribute != LightController.ATTRIBUTE_COLOR_TEMP:
|
||||
raise ValueError(
|
||||
f"Value for `{attribute}` attribute could not be retrieved "
|
||||
f"from `{self.entity.main}`. "
|
||||
"Check the FAQ to know more about this error: "
|
||||
"https://xaviml.github.io/controllerx/faq"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Attribute `{attribute}` with `{value}` as a value "
|
||||
"could not be converted to float"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Attribute `{attribute}` not expected")
|
||||
|
||||
def check_smooth_power_on(
|
||||
self, attribute: str, direction: str, light_state: str
|
||||
) -> bool:
|
||||
return (
|
||||
direction != StepperDir.DOWN
|
||||
and attribute == self.ATTRIBUTE_BRIGHTNESS
|
||||
and self.smooth_power_on
|
||||
and light_state == "off"
|
||||
)
|
||||
|
||||
async def before_action(self, action: str, *args: Any, **kwargs: Any) -> bool:
|
||||
to_return = True
|
||||
self.next_direction = None
|
||||
light_state: str
|
||||
if action in ("click", "hold"):
|
||||
if len(args) == 2:
|
||||
attribute, direction = args
|
||||
elif "attribute" in kwargs and "direction" in kwargs:
|
||||
attribute, direction = kwargs["attribute"], kwargs["direction"]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"`attribute` and `direction` are mandatory fields for `{action}` action"
|
||||
)
|
||||
light_state = await self.get_entity_state()
|
||||
self.smooth_power_on_check = self.check_smooth_power_on(
|
||||
attribute, direction, light_state
|
||||
)
|
||||
self.remove_transition_check = await self.check_remove_transition(
|
||||
on_from_user=False
|
||||
)
|
||||
to_return = (light_state == "on") or self.smooth_power_on_check
|
||||
elif action == "attribute_from_controller_step":
|
||||
light_state = await self.get_entity_state()
|
||||
to_return = light_state == "on"
|
||||
self.smooth_power_on_check = False
|
||||
self.remove_transition_check = False
|
||||
else:
|
||||
self.remove_transition_check = await self.check_remove_transition(
|
||||
on_from_user=True
|
||||
)
|
||||
self.smooth_power_on_check = False
|
||||
return await super().before_action(action, *args, **kwargs) and to_return
|
||||
|
||||
@action
|
||||
async def click(
|
||||
self,
|
||||
attribute: str,
|
||||
direction: str,
|
||||
mode: str = StepperMode.STOP,
|
||||
steps: Number | None = None,
|
||||
) -> None:
|
||||
attribute = self.get_option(
|
||||
attribute, LightController.ATTRIBUTES_LIST, "`click` action"
|
||||
)
|
||||
direction = self.get_option(
|
||||
direction, [StepperDir.UP, StepperDir.DOWN], "`click` action"
|
||||
)
|
||||
mode = self.get_option(
|
||||
mode, [StepperMode.STOP, StepperMode.LOOP], "`click` action"
|
||||
)
|
||||
attribute = await self.get_attribute(attribute)
|
||||
self.value_attribute = await self.get_value_attribute(attribute)
|
||||
await self.change_light_state(
|
||||
self.value_attribute,
|
||||
attribute,
|
||||
direction,
|
||||
self.get_stepper(attribute, steps or self.manual_steps, mode, tag="click"),
|
||||
)
|
||||
|
||||
@action
|
||||
async def hold( # type: ignore[override]
|
||||
self,
|
||||
attribute: str,
|
||||
direction: str,
|
||||
mode: str = StepperMode.STOP,
|
||||
steps: Number | None = None,
|
||||
) -> None:
|
||||
await self._hold(attribute, direction, mode, steps)
|
||||
|
||||
async def _hold(
|
||||
self,
|
||||
attribute: str,
|
||||
direction: str,
|
||||
mode: str = StepperMode.STOP,
|
||||
steps: Number | None = None,
|
||||
) -> None:
|
||||
attribute = self.get_option(
|
||||
attribute, LightController.ATTRIBUTES_LIST, "`hold` action"
|
||||
)
|
||||
direction = self.get_option(
|
||||
direction,
|
||||
[StepperDir.UP, StepperDir.DOWN, StepperDir.TOGGLE],
|
||||
"`hold` action",
|
||||
)
|
||||
mode = self.get_option(
|
||||
mode,
|
||||
[StepperMode.STOP, StepperMode.LOOP, StepperMode.BOUNCE],
|
||||
"`hold` action",
|
||||
)
|
||||
attribute = await self.get_attribute(attribute)
|
||||
self.value_attribute = await self.get_value_attribute(attribute)
|
||||
self.log(
|
||||
f"Attribute value before running the hold action: {self.value_attribute}",
|
||||
level="DEBUG",
|
||||
)
|
||||
stepper = self.get_stepper(
|
||||
attribute, steps or self.automatic_steps, mode, tag="hold"
|
||||
)
|
||||
if direction == StepperDir.TOGGLE:
|
||||
self.log(
|
||||
f"Previous direction: {stepper.previous_direction}",
|
||||
level="DEBUG",
|
||||
)
|
||||
direction = stepper.get_direction(self.value_attribute, direction)
|
||||
self.log(f"Going direction: {direction}", level="DEBUG")
|
||||
await super().hold(attribute, direction, stepper)
|
||||
|
||||
async def hold_loop(
|
||||
self,
|
||||
attribute: str,
|
||||
direction: str,
|
||||
stepper: Stepper,
|
||||
) -> bool:
|
||||
if self.value_attribute is None:
|
||||
return True
|
||||
extra_attributes = {"transition": self.delay / 1000}
|
||||
return await self.change_light_state(
|
||||
self.value_attribute,
|
||||
attribute,
|
||||
direction,
|
||||
stepper,
|
||||
extra_attributes=extra_attributes,
|
||||
)
|
||||
|
||||
async def change_light_state(
|
||||
self,
|
||||
old: float,
|
||||
attribute: str,
|
||||
direction: str,
|
||||
stepper: Stepper,
|
||||
*,
|
||||
extra_attributes: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
This functions changes the state of the light depending on the previous
|
||||
value and attribute. It returns True when no more changes will need to be done.
|
||||
Otherwise, it returns False.
|
||||
"""
|
||||
attributes: dict[str, Any] = (
|
||||
{} if extra_attributes is None else extra_attributes
|
||||
)
|
||||
direction = self.next_direction or direction
|
||||
if attribute == LightController.ATTRIBUTE_XY_COLOR:
|
||||
stepper_output = stepper.step(self.index_color, direction)
|
||||
self.index_color = int(stepper_output.next_value)
|
||||
xy_color = self.color_wheel[self.index_color]
|
||||
attributes[attribute] = list(xy_color)
|
||||
await self._on(**attributes)
|
||||
# In case of xy_color mode it never finishes the loop, the hold loop
|
||||
# will only stop if the hold action is called when releasing the button.
|
||||
# I haven't experimented any problems with it, but a future implementation
|
||||
# would be to force the loop to stop after 4 or 5 loops as a safety measure.
|
||||
return False
|
||||
if self.smooth_power_on_check:
|
||||
await self._on_min(attribute)
|
||||
# # After smooth power on, the light should not brighten up.
|
||||
return True
|
||||
stepper_output = stepper.step(old, direction)
|
||||
self.next_direction = stepper_output.next_direction
|
||||
next_value = int(stepper_output.next_value)
|
||||
attributes[attribute] = next_value
|
||||
await self._on(**attributes)
|
||||
self.value_attribute = next_value
|
||||
return stepper_output.exceeded
|
||||
|
||||
def supports_smooth_power_on(self) -> bool:
|
||||
"""
|
||||
This function can be overrided for each device to indicate the default behaviour of the controller
|
||||
when the associated light is off and an event for incrementing brightness is received.
|
||||
Returns True if the associated light should be turned on with minimum brightness if an event for incrementing
|
||||
brightness is received, while the lamp is off.
|
||||
The behaviour can be overridden by the user with the 'smooth_power_on' option in app configuration.
|
||||
"""
|
||||
return False
|
||||
@@ -0,0 +1,196 @@
|
||||
from typing import Any
|
||||
|
||||
from cx_const import MediaPlayer, Number, PredefinedActionsMapping, StepperDir
|
||||
from cx_core.controller import action
|
||||
from cx_core.feature_support.media_player import MediaPlayerSupport
|
||||
from cx_core.integration import EventData
|
||||
from cx_core.integration.z2m import Z2MIntegration
|
||||
from cx_core.release_hold_controller import ReleaseHoldController
|
||||
from cx_core.stepper import MinMax
|
||||
from cx_core.stepper.index_loop_stepper import IndexLoopStepper
|
||||
from cx_core.stepper.stop_stepper import StopStepper
|
||||
from cx_core.type_controller import Entity, TypeController
|
||||
|
||||
DEFAULT_VOLUME_STEPS = 10
|
||||
|
||||
|
||||
class MediaPlayerController(TypeController[Entity], ReleaseHoldController):
|
||||
domains = ["media_player"]
|
||||
entity_arg = "media_player"
|
||||
volume_level: float
|
||||
|
||||
async def init(self) -> None:
|
||||
volume_steps = self.args.get("volume_steps", DEFAULT_VOLUME_STEPS)
|
||||
self.volume_stepper = StopStepper(MinMax(0, 1), volume_steps)
|
||||
self.volume_level = 0.0
|
||||
await super().init()
|
||||
|
||||
def _get_entity_type(self) -> type[Entity]:
|
||||
return Entity
|
||||
|
||||
def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
|
||||
return {
|
||||
MediaPlayer.HOLD_VOLUME_DOWN: (self.hold, (StepperDir.DOWN,)),
|
||||
MediaPlayer.HOLD_VOLUME_UP: (self.hold, (StepperDir.UP,)),
|
||||
MediaPlayer.CLICK_VOLUME_DOWN: self.volume_down,
|
||||
MediaPlayer.CLICK_VOLUME_UP: self.volume_up,
|
||||
MediaPlayer.VOLUME_SET: self.volume_set,
|
||||
MediaPlayer.RELEASE: self.release,
|
||||
MediaPlayer.PLAY: self.play,
|
||||
MediaPlayer.PAUSE: self.pause,
|
||||
MediaPlayer.PLAY_PAUSE: self.play_pause,
|
||||
MediaPlayer.NEXT_TRACK: self.next_track,
|
||||
MediaPlayer.PREVIOUS_TRACK: self.previous_track,
|
||||
MediaPlayer.NEXT_SOURCE: (self.change_source_list, (StepperDir.UP,)),
|
||||
MediaPlayer.PREVIOUS_SOURCE: (self.change_source_list, (StepperDir.DOWN,)),
|
||||
MediaPlayer.MUTE: self.volume_mute,
|
||||
MediaPlayer.TTS: self.tts,
|
||||
MediaPlayer.VOLUME_FROM_CONTROLLER_ANGLE: self.volume_from_controller_angle,
|
||||
}
|
||||
|
||||
@action
|
||||
async def change_source_list(self, direction: str) -> None:
|
||||
entity_states = await self.get_entity_state(attribute="all")
|
||||
entity_attributes = entity_states["attributes"]
|
||||
source_list: list[str] | None = entity_attributes.get("source_list")
|
||||
if source_list is None or len(source_list) == 0:
|
||||
self.log(
|
||||
f"⚠️ There is no `source_list` parameter in `{self.entity}`",
|
||||
level="WARNING",
|
||||
ascii_encode=False,
|
||||
)
|
||||
return
|
||||
source = entity_attributes.get("source")
|
||||
new_index_source: Number
|
||||
if source is None:
|
||||
new_index_source = 0
|
||||
else:
|
||||
index_source = source_list.index(source)
|
||||
source_stepper = IndexLoopStepper(len(source_list))
|
||||
stepper_output = source_stepper.step(index_source, direction)
|
||||
new_index_source = stepper_output.next_value
|
||||
await self.call_service(
|
||||
"media_player/select_source",
|
||||
entity_id=self.entity.name,
|
||||
source=source_list[int(new_index_source)],
|
||||
)
|
||||
|
||||
@action
|
||||
async def play(self) -> None:
|
||||
await self.call_service("media_player/media_play", entity_id=self.entity.name)
|
||||
|
||||
@action
|
||||
async def pause(self) -> None:
|
||||
await self.call_service("media_player/media_pause", entity_id=self.entity.name)
|
||||
|
||||
@action
|
||||
async def play_pause(self) -> None:
|
||||
await self.call_service(
|
||||
"media_player/media_play_pause", entity_id=self.entity.name
|
||||
)
|
||||
|
||||
@action
|
||||
async def previous_track(self) -> None:
|
||||
await self.call_service(
|
||||
"media_player/media_previous_track", entity_id=self.entity.name
|
||||
)
|
||||
|
||||
@action
|
||||
async def next_track(self) -> None:
|
||||
await self.call_service(
|
||||
"media_player/media_next_track", entity_id=self.entity.name
|
||||
)
|
||||
|
||||
@action
|
||||
async def volume_set(self, volume_level: float) -> None:
|
||||
await self.call_service(
|
||||
"media_player/volume_set",
|
||||
entity_id=self.entity.name,
|
||||
volume_level=volume_level,
|
||||
)
|
||||
|
||||
@action
|
||||
async def volume_up(self) -> None:
|
||||
await self.prepare_volume_change()
|
||||
await self.volume_change(StepperDir.UP)
|
||||
|
||||
@action
|
||||
async def volume_down(self) -> None:
|
||||
await self.prepare_volume_change()
|
||||
await self.volume_change(StepperDir.DOWN)
|
||||
|
||||
@action
|
||||
async def volume_mute(self) -> None:
|
||||
await self.call_service("media_player/volume_mute", entity_id=self.entity.name)
|
||||
|
||||
@action
|
||||
async def tts(
|
||||
self,
|
||||
message: str,
|
||||
service: str = "google_translate_say",
|
||||
cache: bool | None = None,
|
||||
language: str | None = None,
|
||||
options: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
args: dict[str, Any] = {"entity_id": self.entity.name, "message": message}
|
||||
if cache is not None:
|
||||
args["cache"] = cache
|
||||
if language is not None:
|
||||
args["language"] = language
|
||||
if options is not None:
|
||||
args["options"] = options
|
||||
await self.call_service(f"tts.{service}", **args)
|
||||
|
||||
@action
|
||||
async def volume_from_controller_angle(
|
||||
self, extra: EventData | None = None
|
||||
) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, Z2MIntegration):
|
||||
if "action_rotation_angle" not in extra:
|
||||
self.log(
|
||||
"`action_rotation_angle` is not present in the MQTT payload",
|
||||
level="WARNING",
|
||||
)
|
||||
return
|
||||
angle = extra["action_rotation_angle"]
|
||||
direction = StepperDir.UP if angle > 0 else StepperDir.DOWN
|
||||
await self._hold(direction)
|
||||
|
||||
@action
|
||||
async def hold(self, direction: str) -> None: # type: ignore[override]
|
||||
await self._hold(direction)
|
||||
|
||||
async def _hold(self, direction: str) -> None:
|
||||
await self.prepare_volume_change()
|
||||
await super().hold(direction)
|
||||
|
||||
async def prepare_volume_change(self) -> None:
|
||||
volume_level = await self.get_entity_state(attribute="volume_level")
|
||||
if volume_level is not None:
|
||||
self.volume_level = volume_level
|
||||
|
||||
async def volume_change(self, direction: str) -> bool:
|
||||
if await self.feature_support.is_supported(MediaPlayerSupport.VOLUME_SET):
|
||||
stepper_output = self.volume_stepper.step(self.volume_level, direction)
|
||||
self.volume_level = stepper_output.next_value
|
||||
await self.volume_set(self.volume_level)
|
||||
return stepper_output.exceeded
|
||||
else:
|
||||
if direction == StepperDir.UP:
|
||||
await self.call_service(
|
||||
"media_player/volume_up", entity_id=self.entity.name
|
||||
)
|
||||
else:
|
||||
await self.call_service(
|
||||
"media_player/volume_down", entity_id=self.entity.name
|
||||
)
|
||||
return False
|
||||
|
||||
async def hold_loop(self, direction: str) -> bool:
|
||||
return await self.volume_change(direction)
|
||||
|
||||
def default_delay(self) -> int:
|
||||
return 500
|
||||
@@ -0,0 +1,48 @@
|
||||
from cx_const import PredefinedActionsMapping, Switch
|
||||
from cx_core.controller import action
|
||||
from cx_core.type_controller import Entity, TypeController
|
||||
|
||||
|
||||
class SwitchController(TypeController[Entity]):
|
||||
"""
|
||||
This is the main class that controls the switches for different devices.
|
||||
Type of actions:
|
||||
- On/Off/Toggle
|
||||
Parameters taken:
|
||||
- controller (required): Inherited from Controller
|
||||
- switch (required): Switch entity name
|
||||
"""
|
||||
|
||||
domains = [
|
||||
"switch",
|
||||
"alert",
|
||||
"automation",
|
||||
"cover",
|
||||
"input_boolean",
|
||||
"light",
|
||||
"media_player",
|
||||
"script",
|
||||
]
|
||||
entity_arg = "switch"
|
||||
|
||||
def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
|
||||
return {
|
||||
Switch.ON: self.on,
|
||||
Switch.OFF: self.off,
|
||||
Switch.TOGGLE: self.toggle,
|
||||
}
|
||||
|
||||
def _get_entity_type(self) -> type[Entity]:
|
||||
return Entity
|
||||
|
||||
@action
|
||||
async def on(self) -> None:
|
||||
await self.call_service("homeassistant/turn_on", entity_id=self.entity.name)
|
||||
|
||||
@action
|
||||
async def off(self) -> None:
|
||||
await self.call_service("homeassistant/turn_off", entity_id=self.entity.name)
|
||||
|
||||
@action
|
||||
async def toggle(self) -> None:
|
||||
await self.call_service("homeassistant/toggle", entity_id=self.entity.name)
|
||||
@@ -0,0 +1,429 @@
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable
|
||||
from functools import lru_cache
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from cx_const import PredefinedActionsMapping, StepperDir, Z2MLight
|
||||
from cx_core.controller import Controller, action
|
||||
from cx_core.integration import EventData
|
||||
from cx_core.integration.z2m import Z2MIntegration
|
||||
from cx_core.stepper import InvertStepper, MinMax
|
||||
from cx_core.type_controller import Entity, TypeController
|
||||
|
||||
DEFAULT_CLICK_STEPS = 70
|
||||
DEFAULT_HOLD_STEPS = 70
|
||||
DEFAULT_TRANSITION = 0.5
|
||||
DEFAULT_MODE = "ha"
|
||||
DEFAULT_TOPIC_PREFIX = "zigbee2mqtt"
|
||||
|
||||
Mode = Annotated[str, Literal["ha", "mqtt"]]
|
||||
|
||||
|
||||
class Z2MLightEntity(Entity):
|
||||
mode: Mode
|
||||
topic_prefix: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
entities: list[str] | None = None,
|
||||
mode: Mode = DEFAULT_MODE,
|
||||
topic_prefix: str = DEFAULT_TOPIC_PREFIX,
|
||||
) -> None:
|
||||
super().__init__(name, entities)
|
||||
mode = Controller.get_option(mode, ["ha", "mqtt"])
|
||||
self.mode = mode
|
||||
self.topic_prefix = topic_prefix
|
||||
|
||||
|
||||
class Z2MLightController(TypeController[Z2MLightEntity]):
|
||||
"""
|
||||
This is the main class that controls the Zigbee2MQTT lights for different devices.
|
||||
Type of actions:
|
||||
- On/Off/Toggle
|
||||
- Brightness click and hold
|
||||
- Color temp click and hold
|
||||
"""
|
||||
|
||||
ATTRIBUTE_BRIGHTNESS = "brightness"
|
||||
ATTRIBUTE_COLOR_TEMP = "color_temp"
|
||||
|
||||
ATTRIBUTES_LIST = [
|
||||
ATTRIBUTE_BRIGHTNESS,
|
||||
ATTRIBUTE_COLOR_TEMP,
|
||||
]
|
||||
|
||||
MIN_MAX_ATTR = {
|
||||
ATTRIBUTE_BRIGHTNESS: MinMax(min=1, max=254),
|
||||
ATTRIBUTE_COLOR_TEMP: MinMax(min=250, max=454),
|
||||
}
|
||||
|
||||
entity_arg = "light"
|
||||
|
||||
click_steps: float
|
||||
hold_steps: float
|
||||
transition: float
|
||||
use_onoff: bool
|
||||
|
||||
hold_attribute: str | None
|
||||
|
||||
_mqtt_fn: dict[Mode, Callable[[str, str], Awaitable[None]]]
|
||||
|
||||
async def init(self) -> None:
|
||||
self.click_steps = self.args.get("click_steps", DEFAULT_CLICK_STEPS)
|
||||
self.hold_steps = self.args.get("hold_steps", DEFAULT_HOLD_STEPS)
|
||||
self.transition = self.args.get("transition", DEFAULT_TRANSITION)
|
||||
self.use_onoff = self.args.get("use_onoff", False)
|
||||
|
||||
self._mqtt_fn = {
|
||||
"ha": self._ha_mqtt_call,
|
||||
"mqtt": self._mqtt_plugin_call,
|
||||
}
|
||||
self.hold_attribute = None
|
||||
|
||||
await super().init()
|
||||
|
||||
def _get_entity_type(self) -> type[Z2MLightEntity]:
|
||||
return Z2MLightEntity
|
||||
|
||||
def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
|
||||
return {
|
||||
Z2MLight.ON: self.on,
|
||||
Z2MLight.OFF: self.off,
|
||||
Z2MLight.TOGGLE: self.toggle,
|
||||
Z2MLight.RELEASE: self.release,
|
||||
Z2MLight.ON_FULL_BRIGHTNESS: (
|
||||
self.on_full,
|
||||
(Z2MLightController.ATTRIBUTE_BRIGHTNESS,),
|
||||
),
|
||||
Z2MLight.ON_FULL_COLOR_TEMP: (
|
||||
self.on_full,
|
||||
(Z2MLightController.ATTRIBUTE_COLOR_TEMP,),
|
||||
),
|
||||
Z2MLight.ON_MIN_BRIGHTNESS: (
|
||||
self.on_min,
|
||||
(Z2MLightController.ATTRIBUTE_BRIGHTNESS,),
|
||||
),
|
||||
Z2MLight.ON_MIN_COLOR_TEMP: (
|
||||
self.on_min,
|
||||
(Z2MLightController.ATTRIBUTE_COLOR_TEMP,),
|
||||
),
|
||||
Z2MLight.SET_HALF_BRIGHTNESS: (
|
||||
self.set_value,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_BRIGHTNESS,
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
Z2MLight.SET_HALF_COLOR_TEMP: (
|
||||
self.set_value,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_COLOR_TEMP,
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
Z2MLight.CLICK: self.click,
|
||||
Z2MLight.CLICK_BRIGHTNESS_UP: (
|
||||
self.click,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Z2MLight.CLICK_COLOR_TEMP_UP: (
|
||||
self.click,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Z2MLight.CLICK_BRIGHTNESS_DOWN: (
|
||||
self.click,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Z2MLight.CLICK_COLOR_TEMP_DOWN: (
|
||||
self.click,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Z2MLight.HOLD: self.hold,
|
||||
Z2MLight.HOLD_BRIGHTNESS_UP: (
|
||||
self.hold,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Z2MLight.HOLD_COLOR_TEMP_UP: (
|
||||
self.hold,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.UP,
|
||||
),
|
||||
),
|
||||
Z2MLight.HOLD_BRIGHTNESS_DOWN: (
|
||||
self.hold,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Z2MLight.HOLD_BRIGHTNESS_TOGGLE: (
|
||||
self.hold,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_BRIGHTNESS,
|
||||
StepperDir.TOGGLE,
|
||||
),
|
||||
),
|
||||
Z2MLight.HOLD_COLOR_TEMP_DOWN: (
|
||||
self.hold,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.DOWN,
|
||||
),
|
||||
),
|
||||
Z2MLight.HOLD_COLOR_TEMP_TOGGLE: (
|
||||
self.hold,
|
||||
(
|
||||
Z2MLightController.ATTRIBUTE_COLOR_TEMP,
|
||||
StepperDir.TOGGLE,
|
||||
),
|
||||
),
|
||||
Z2MLight.XYCOLOR_FROM_CONTROLLER: self.xycolor_from_controller,
|
||||
Z2MLight.COLORTEMP_FROM_CONTROLLER: self.colortemp_from_controller,
|
||||
Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL: self.brightness_from_controller_level,
|
||||
Z2MLight.BRIGHTNESS_FROM_CONTROLLER_ANGLE: self.brightness_from_controller_angle,
|
||||
}
|
||||
|
||||
async def before_action(self, action: str, *args: Any, **kwargs: Any) -> bool:
|
||||
to_return = not (action == "hold" and self.hold_attribute is not None)
|
||||
return await super().before_action(action, *args, **kwargs) and to_return
|
||||
|
||||
async def _ha_mqtt_call(self, topic: str, payload: str) -> None:
|
||||
await self.call_service("mqtt.publish", topic=topic, payload=payload)
|
||||
|
||||
async def _mqtt_plugin_call(self, topic: str, payload: str) -> None:
|
||||
await self.call_service(
|
||||
"mqtt.publish", topic=topic, payload=payload, namespace="mqtt"
|
||||
)
|
||||
|
||||
async def _mqtt_call(self, payload: dict[str, Any]) -> None:
|
||||
await self._mqtt_fn[self.entity.mode](
|
||||
f"{self.entity.topic_prefix}/{self.entity.name}/set", json.dumps(payload)
|
||||
)
|
||||
|
||||
async def _on(self, **attributes: Any) -> None:
|
||||
await self._mqtt_call({"state": "ON", **attributes})
|
||||
|
||||
@action
|
||||
async def on(self, attributes: dict[str, float] | None = None) -> None:
|
||||
attributes = attributes or {}
|
||||
await self._on(**attributes)
|
||||
|
||||
async def _off(self) -> None:
|
||||
await self._mqtt_call({"state": "OFF"})
|
||||
|
||||
@action
|
||||
async def off(self) -> None:
|
||||
await self._off()
|
||||
|
||||
async def _toggle(self, **attributes: Any) -> None:
|
||||
await self._mqtt_call({"state": "TOGGLE", **attributes})
|
||||
|
||||
@action
|
||||
async def toggle(self, attributes: dict[str, float] | None = None) -> None:
|
||||
attributes = attributes or {}
|
||||
await self._toggle(**attributes)
|
||||
|
||||
async def _set_value(self, attribute: str, fraction: float) -> None:
|
||||
fraction = max(0, min(fraction, 1))
|
||||
min_ = self.MIN_MAX_ATTR[attribute].min
|
||||
max_ = self.MIN_MAX_ATTR[attribute].max
|
||||
value = (max_ - min_) * fraction + min_
|
||||
await self._on(**{attribute: value})
|
||||
|
||||
@action
|
||||
async def set_value(self, attribute: str, fraction: float) -> None:
|
||||
await self._set_value(attribute, fraction)
|
||||
|
||||
async def _on_full(self, attribute: str) -> None:
|
||||
await self._set_value(attribute, 1)
|
||||
|
||||
@action
|
||||
async def on_full(self, attribute: str) -> None:
|
||||
await self._on_full(attribute)
|
||||
|
||||
async def _on_min(self, attribute: str) -> None:
|
||||
await self._set_value(attribute, 0)
|
||||
|
||||
@action
|
||||
async def on_min(self, attribute: str) -> None:
|
||||
await self._on_min(attribute)
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_stepper(self, attribute: str, steps: float, *, tag: str) -> InvertStepper:
|
||||
previous_direction = StepperDir.DOWN
|
||||
return InvertStepper(self.MIN_MAX_ATTR[attribute], steps, previous_direction)
|
||||
|
||||
async def _change_light_state(
|
||||
self,
|
||||
*,
|
||||
attribute: str,
|
||||
direction: str,
|
||||
stepper: InvertStepper,
|
||||
transition: float | None,
|
||||
use_onoff: bool,
|
||||
mode: str,
|
||||
) -> None:
|
||||
onoff_cmd = (
|
||||
"_onoff" if use_onoff and attribute == self.ATTRIBUTE_BRIGHTNESS else ""
|
||||
)
|
||||
stepper_output = stepper.step(stepper.steps, direction)
|
||||
kwargs = {}
|
||||
if transition is not None:
|
||||
kwargs["transition"] = transition
|
||||
await self._mqtt_call(
|
||||
{
|
||||
f"{attribute}_{mode}{onoff_cmd}": stepper_output.next_value,
|
||||
**kwargs,
|
||||
}
|
||||
)
|
||||
|
||||
@action
|
||||
async def click(
|
||||
self,
|
||||
attribute: str,
|
||||
direction: str,
|
||||
steps: float | None = None,
|
||||
transition: float | None = None,
|
||||
use_onoff: bool | None = None,
|
||||
) -> None:
|
||||
attribute = self.get_option(attribute, self.ATTRIBUTES_LIST, "`click` action")
|
||||
direction = self.get_option(
|
||||
direction, [StepperDir.UP, StepperDir.DOWN], "`click` action"
|
||||
)
|
||||
steps = steps if steps is not None else self.click_steps
|
||||
stepper = self.get_stepper(attribute, steps, tag="click")
|
||||
await self._change_light_state(
|
||||
attribute=attribute,
|
||||
direction=direction,
|
||||
stepper=stepper,
|
||||
transition=transition if transition is not None else self.transition,
|
||||
use_onoff=use_onoff if use_onoff is not None else self.use_onoff,
|
||||
mode="step",
|
||||
)
|
||||
|
||||
async def _hold(
|
||||
self,
|
||||
attribute: str,
|
||||
direction: str,
|
||||
steps: float | None = None,
|
||||
use_onoff: bool | None = None,
|
||||
) -> None:
|
||||
attribute = self.get_option(attribute, self.ATTRIBUTES_LIST, "`hold` action")
|
||||
direction = self.get_option(
|
||||
direction,
|
||||
[StepperDir.UP, StepperDir.DOWN, StepperDir.TOGGLE],
|
||||
"`hold` action",
|
||||
)
|
||||
steps = steps if steps is not None else self.hold_steps
|
||||
stepper = self.get_stepper(attribute, steps, tag="hold")
|
||||
direction = stepper.get_direction(steps, direction)
|
||||
self.hold_attribute = attribute
|
||||
await self._change_light_state(
|
||||
attribute=attribute,
|
||||
direction=direction,
|
||||
stepper=stepper,
|
||||
transition=None,
|
||||
use_onoff=use_onoff if use_onoff is not None else self.use_onoff,
|
||||
mode="move",
|
||||
)
|
||||
|
||||
@action
|
||||
async def hold(
|
||||
self,
|
||||
attribute: str,
|
||||
direction: str,
|
||||
steps: float | None = None,
|
||||
use_onoff: bool | None = None,
|
||||
) -> None:
|
||||
await self._hold(attribute, direction, steps, use_onoff)
|
||||
|
||||
@action
|
||||
async def release(self) -> None:
|
||||
if self.hold_attribute is None:
|
||||
return
|
||||
await self._mqtt_call({f"{self.hold_attribute}_move": "stop"})
|
||||
self.hold_attribute = None
|
||||
|
||||
@action
|
||||
async def xycolor_from_controller(self, extra: EventData | None = None) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, Z2MIntegration):
|
||||
if "action_color" not in extra:
|
||||
self.log(
|
||||
"`action_color` is not present in the MQTT payload", level="WARNING"
|
||||
)
|
||||
return
|
||||
xy_color = extra["action_color"]
|
||||
await self._on(color={"x": xy_color["x"], "y": xy_color["y"]})
|
||||
|
||||
@action
|
||||
async def colortemp_from_controller(self, extra: EventData | None = None) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, Z2MIntegration):
|
||||
if "action_color_temperature" not in extra:
|
||||
self.log(
|
||||
"`action_color_temperature` is not present in the MQTT payload",
|
||||
level="WARNING",
|
||||
)
|
||||
return
|
||||
await self._on(color_temp=extra["action_color_temperature"])
|
||||
|
||||
@action
|
||||
async def brightness_from_controller_level(
|
||||
self, extra: EventData | None = None
|
||||
) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, Z2MIntegration):
|
||||
if "action_level" not in extra:
|
||||
self.log(
|
||||
"`action_level` is not present in the MQTT payload",
|
||||
level="WARNING",
|
||||
)
|
||||
return
|
||||
await self._on(brightness=extra["action_level"])
|
||||
|
||||
@action
|
||||
async def brightness_from_controller_angle(
|
||||
self,
|
||||
steps: float | None = None,
|
||||
use_onoff: bool | None = None,
|
||||
extra: EventData | None = None,
|
||||
) -> None:
|
||||
if extra is None:
|
||||
self.log("No event data present", level="WARNING")
|
||||
return
|
||||
if isinstance(self.integration, Z2MIntegration):
|
||||
if "action_rotation_angle" not in extra:
|
||||
self.log(
|
||||
"`action_rotation_angle` is not present in the MQTT payload",
|
||||
level="WARNING",
|
||||
)
|
||||
return
|
||||
angle = extra["action_rotation_angle"]
|
||||
direction = StepperDir.UP if angle > 0 else StepperDir.DOWN
|
||||
await self._hold(
|
||||
self.ATTRIBUTE_BRIGHTNESS, direction, steps=steps, use_onoff=use_onoff
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
import abc
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from cx_core.controller import Controller
|
||||
from cx_core.feature_support import FeatureSupport
|
||||
|
||||
EntityVar = TypeVar("EntityVar", bound="Entity")
|
||||
|
||||
|
||||
class Entity:
|
||||
name: str
|
||||
entities: list[str]
|
||||
|
||||
def __init__(
|
||||
self, name: str, entities: list[str] | None = None, **kwargs: Any
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.set_entities(entities)
|
||||
|
||||
@property
|
||||
def main(self) -> str:
|
||||
return self.entities[0]
|
||||
|
||||
@property
|
||||
def is_group(self) -> bool:
|
||||
return self.entities[0] != self.name
|
||||
|
||||
def set_entities(self, value: list[str] | None = None) -> None:
|
||||
self.entities = value if value is not None else [self.name]
|
||||
|
||||
@classmethod
|
||||
def instantiate(
|
||||
cls: type[EntityVar],
|
||||
name: str,
|
||||
entities: list[str] | None = None,
|
||||
**params: Any,
|
||||
) -> EntityVar:
|
||||
return cls(name=name, entities=entities, **params)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name if not self.is_group else f"{self.name}({self.entities})"
|
||||
|
||||
|
||||
class TypeController(Controller, abc.ABC, Generic[EntityVar]):
|
||||
domains: list[str] = []
|
||||
entity_arg: str
|
||||
entity: EntityVar
|
||||
update_supported_features: bool
|
||||
feature_support: FeatureSupport
|
||||
|
||||
async def init(self) -> None:
|
||||
if self.entity_arg not in self.args:
|
||||
raise ValueError(
|
||||
f"{self.__class__.__name__} class needs the `{self.entity_arg}` attribute"
|
||||
)
|
||||
self.entity = await self._get_entity(self.args[self.entity_arg])
|
||||
self._check_domain(self.entity)
|
||||
self.update_supported_features = self.args.get(
|
||||
"update_supported_features", False
|
||||
)
|
||||
supported_features: int | None = self.args.get("supported_features")
|
||||
self.feature_support = FeatureSupport(
|
||||
self, supported_features, self.update_supported_features
|
||||
)
|
||||
await super().init()
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_entity_type(self) -> type[EntityVar]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def _get_entities(self, entity_name: str) -> list[str] | None:
|
||||
entities: str | list[str] | None = await self.get_state(
|
||||
entity_name, attribute="entity_id"
|
||||
)
|
||||
self.log(
|
||||
f"Entities from `{entity_name}` (entity_id attribute): `{entities}`",
|
||||
level="DEBUG",
|
||||
)
|
||||
# If the entity groups other entities, this attribute will be a list
|
||||
if not isinstance(entities, (list, tuple)):
|
||||
return None
|
||||
if entities is not None and len(entities) == 0:
|
||||
raise ValueError(f"`{entity_name}` does not have any entities registered.")
|
||||
return entities
|
||||
|
||||
async def _get_entity(self, entity: str | dict[str, Any]) -> EntityVar:
|
||||
entity_args: dict[str, Any]
|
||||
entity_name: str
|
||||
if isinstance(entity, str):
|
||||
entity_name = entity
|
||||
entity_args = {}
|
||||
elif isinstance(entity, dict):
|
||||
entity_name = entity["name"]
|
||||
entity_args = {key: value for key, value in entity.items() if key != "name"}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Type {type(entity)} is not supported for `{self.entity_arg}` attribute"
|
||||
)
|
||||
entities = await self._get_entities(entity_name) if self.domains else None
|
||||
return self._get_entity_type().instantiate(
|
||||
name=entity_name, entities=entities, **entity_args
|
||||
)
|
||||
|
||||
def _check_domain(self, entity: Entity) -> None:
|
||||
if not self.domains:
|
||||
return
|
||||
if self.contains_templating(entity.name):
|
||||
return
|
||||
same_domain = all(
|
||||
any(elem.startswith(domain + ".") for domain in self.domains)
|
||||
for elem in entity.entities
|
||||
)
|
||||
if not same_domain:
|
||||
if entity.is_group:
|
||||
error_msg = (
|
||||
f"All the subentities from {entity} must be from one "
|
||||
f"of the following domains {self.domains} (e.g. {self.domains[0]}.bedroom)"
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
f"'{entity}' must be from one "
|
||||
f"of the following domains {self.domains} (e.g. {self.domains[0]}.bedroom)"
|
||||
)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
async def get_entity_state(self, attribute: str | None = None) -> Any:
|
||||
entity = self.entity.main
|
||||
if self.update_supported_features:
|
||||
entities = await self._get_entities(self.entity.name)
|
||||
self.entity.set_entities(entities)
|
||||
entity = self.entity.entities[0]
|
||||
out = await self.get_state(entity, attribute=attribute)
|
||||
return out
|
||||
@@ -0,0 +1,46 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
|
||||
|
||||
class AdeoHRC99CZC045LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"off": Light.OFF,
|
||||
"brightness_step_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_step_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
"color_hue_step_up": Light.HOLD_COLOR_UP,
|
||||
"color_hue_step_down": Light.HOLD_COLOR_DOWN,
|
||||
"color_saturation_step_up": Light.HOLD_COLOR_UP,
|
||||
"color_saturation_step_down": Light.HOLD_COLOR_DOWN,
|
||||
"color_temperature_step_up": Light.HOLD_COLOR_TEMP_UP,
|
||||
"color_temperature_step_down": Light.HOLD_COLOR_TEMP_DOWN,
|
||||
"color_stop": Light.RELEASE,
|
||||
"scene_1": None,
|
||||
"scene_2": None,
|
||||
"scene_3": None,
|
||||
"scene_4": None,
|
||||
}
|
||||
|
||||
|
||||
class AdeoHRC99CZC045Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.ON,
|
||||
"off": Z2MLight.OFF,
|
||||
"brightness_step_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_step_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
"color_hue_step_up": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"color_hue_step_down": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"color_saturation_step_up": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"color_saturation_step_down": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"color_temperature_step_up": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"color_temperature_step_down": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"color_stop": Z2MLight.RELEASE,
|
||||
"scene_1": None,
|
||||
"scene_2": None,
|
||||
"scene_3": None,
|
||||
"scene_4": None,
|
||||
}
|
||||
@@ -0,0 +1,878 @@
|
||||
from cx_const import DefaultActionsMapping, Light, MediaPlayer, Switch, Z2MLight
|
||||
from cx_core import (
|
||||
LightController,
|
||||
MediaPlayerController,
|
||||
SwitchController,
|
||||
Z2MLightController,
|
||||
)
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class WXKG02LMLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_both": Light.TOGGLE,
|
||||
"double_both": Light.CLICK_BRIGHTNESS_UP,
|
||||
"hold_both": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"single_left": Light.TOGGLE,
|
||||
"double_left": Light.CLICK_BRIGHTNESS_UP,
|
||||
"hold_left": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"single_right": Light.TOGGLE,
|
||||
"double_right": Light.CLICK_BRIGHTNESS_UP,
|
||||
"hold_right": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE, # single left
|
||||
1001: Light.CLICK_BRIGHTNESS_DOWN, # long left
|
||||
1004: Light.CLICK_BRIGHTNESS_UP, # double left
|
||||
2002: Light.TOGGLE, # single right
|
||||
2001: Light.CLICK_BRIGHTNESS_DOWN, # long right
|
||||
2004: Light.CLICK_BRIGHTNESS_UP, # double right
|
||||
3002: Light.TOGGLE, # single both
|
||||
3001: Light.CLICK_BRIGHTNESS_DOWN, # long both
|
||||
3004: Light.CLICK_BRIGHTNESS_UP, # double both
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"both_single": Light.TOGGLE,
|
||||
"both_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"both_long press": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"left_single": Light.TOGGLE,
|
||||
"left_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"left_long press": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"right_single": Light.TOGGLE,
|
||||
"right_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"right_long press": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class WXKG02LMZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_both": Z2MLight.TOGGLE,
|
||||
"double_both": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"hold_both": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"single_left": Z2MLight.TOGGLE,
|
||||
"double_left": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"hold_left": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"single_right": Z2MLight.TOGGLE,
|
||||
"double_right": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"hold_right": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
|
||||
class WXKG02LMSwitchController(SwitchController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_both": Switch.TOGGLE,
|
||||
"single_left": Switch.TOGGLE,
|
||||
"single_right": Switch.TOGGLE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Switch.TOGGLE,
|
||||
2002: Switch.TOGGLE,
|
||||
3002: Switch.TOGGLE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"both_single": Switch.TOGGLE,
|
||||
"left_single": Switch.TOGGLE,
|
||||
"right_single": Switch.TOGGLE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class WXKG01LMLightController(LightController):
|
||||
"""
|
||||
Different states reported from the controller:
|
||||
single, double, triple, quadruple,
|
||||
many, hold, release
|
||||
"""
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
"triple": Light.ON_MIN_BRIGHTNESS,
|
||||
"quadruple": Light.SET_HALF_BRIGHTNESS,
|
||||
"many": None,
|
||||
"hold": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE, # single
|
||||
1004: Light.ON_FULL_BRIGHTNESS, # double
|
||||
1005: Light.ON_MIN_BRIGHTNESS, # triple
|
||||
1006: Light.SET_HALF_BRIGHTNESS, # quadruple
|
||||
1010: None, # many
|
||||
1001: Light.HOLD_BRIGHTNESS_TOGGLE, # hold the button
|
||||
1003: Light.RELEASE, # release the button
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
"triple": Light.ON_MIN_BRIGHTNESS,
|
||||
"quadruple": Light.SET_HALF_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
args = data["args"]
|
||||
if "click_type" in args:
|
||||
return args["click_type"]
|
||||
return data["command"]
|
||||
|
||||
|
||||
class WXKG01LMZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Z2MLight.TOGGLE,
|
||||
"double": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"triple": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
"quadruple": Z2MLight.SET_HALF_BRIGHTNESS,
|
||||
"many": None,
|
||||
"hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class WXKG11LMRemoteLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
"hold": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE,
|
||||
1004: Light.ON_FULL_BRIGHTNESS,
|
||||
1001: Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
1003: Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
"hold": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class WXKG11LMRemoteZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Z2MLight.TOGGLE,
|
||||
"double": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class WXKG11LMSensorSwitchLightController(LightController):
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE,
|
||||
1004: Light.ON_FULL_BRIGHTNESS,
|
||||
1005: Light.ON_MIN_BRIGHTNESS,
|
||||
1006: Light.SET_HALF_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
"triple": Light.ON_MIN_BRIGHTNESS,
|
||||
"quadruple": Light.SET_HALF_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
mapping = {
|
||||
1: "single",
|
||||
2: "double",
|
||||
3: "triple",
|
||||
4: "quadruple",
|
||||
}
|
||||
clicks = data["args"]["value"]
|
||||
return mapping.get(clicks, "")
|
||||
|
||||
|
||||
class WXKG12LMLightController(LightController):
|
||||
"""
|
||||
Different states reported from the controller:
|
||||
single, double, shake, hold, release
|
||||
"""
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
"shake": Light.ON_MIN_BRIGHTNESS,
|
||||
"hold": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE, # button_1_press
|
||||
1004: Light.ON_FULL_BRIGHTNESS, # button_1_double_press
|
||||
1007: Light.ON_MIN_BRIGHTNESS, # button_1_shake
|
||||
1001: Light.HOLD_BRIGHTNESS_TOGGLE, # button_1_hold
|
||||
1003: Light.RELEASE, # button_1_release_after_hold
|
||||
}
|
||||
|
||||
|
||||
class WXKG12LMZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Z2MLight.TOGGLE,
|
||||
"double": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"shake": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
"hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class MFKZQ01LMLightController(LightController):
|
||||
"""
|
||||
This controller allows movement actions for Xiaomi Aqara Smart Cube as
|
||||
shake, wakeup, fall, slide, flip180 or 90 and rotate_left or right.
|
||||
Then from the apps.yaml the needed actions can be included and create
|
||||
different instances for different lights.
|
||||
"""
|
||||
|
||||
# Different states reported from the controller:
|
||||
# shake, wakeup, fall, tap, slide, flip180
|
||||
# flip90, rotate_left and rotate_right
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"shake": Light.ON_MIN_BRIGHTNESS,
|
||||
"tap": Light.TOGGLE,
|
||||
"slide": Light.ON_FULL_BRIGHTNESS,
|
||||
"flip180": Light.CLICK_COLOR_UP,
|
||||
"flip90": Light.CLICK_COLOR_DOWN,
|
||||
"rotate_left": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"rotate_right": Light.CLICK_BRIGHTNESS_UP,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1: Light.ON_MIN_BRIGHTNESS,
|
||||
6: Light.TOGGLE,
|
||||
5: Light.ON_FULL_BRIGHTNESS,
|
||||
4: Light.CLICK_COLOR_UP,
|
||||
3: Light.CLICK_COLOR_DOWN,
|
||||
8: Light.CLICK_BRIGHTNESS_DOWN,
|
||||
7: Light.CLICK_BRIGHTNESS_UP,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"shake": Light.ON_MIN_BRIGHTNESS,
|
||||
"knock": Light.TOGGLE,
|
||||
"slide": Light.ON_FULL_BRIGHTNESS,
|
||||
"flip180": Light.CLICK_COLOR_UP,
|
||||
"flip90": Light.CLICK_COLOR_DOWN,
|
||||
"rotate_left": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"rotate_right": Light.CLICK_BRIGHTNESS_UP,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
action = command
|
||||
args = data.get("args", {})
|
||||
if command == "flip":
|
||||
action = command + str(args["flip_degrees"])
|
||||
return action
|
||||
|
||||
|
||||
class WXCJKG11LMLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_1_single": Light.OFF,
|
||||
"button_1_double": Light.ON_MIN_BRIGHTNESS,
|
||||
"button_1_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"button_1_release": Light.RELEASE,
|
||||
"button_2_single": Light.ON,
|
||||
"button_2_double": Light.ON_FULL_BRIGHTNESS,
|
||||
"button_2_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"button_2_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.OFF,
|
||||
1001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
1003: Light.RELEASE,
|
||||
2002: Light.ON,
|
||||
2001: Light.HOLD_BRIGHTNESS_UP,
|
||||
2003: Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class WXCJKG11LMZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_1_single": Z2MLight.OFF,
|
||||
"button_1_double": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
"button_1_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"button_1_release": Z2MLight.RELEASE,
|
||||
"button_2_single": Z2MLight.ON,
|
||||
"button_2_double": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"button_2_hold": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"button_2_release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class WXCJKG12LMLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_1_single": Light.OFF,
|
||||
"button_1_double": Light.ON_MIN_COLOR_TEMP,
|
||||
"button_1_triple": None,
|
||||
"button_1_hold": Light.HOLD_COLOR_DOWN,
|
||||
"button_1_release": Light.RELEASE,
|
||||
"button_2_single": Light.ON,
|
||||
"button_2_double": Light.ON_FULL_COLOR_TEMP,
|
||||
"button_2_triple": None,
|
||||
"button_2_hold": Light.HOLD_COLOR_UP,
|
||||
"button_2_release": Light.RELEASE,
|
||||
"button_3_single": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"button_3_double": Light.ON_MIN_BRIGHTNESS,
|
||||
"button_3_triple": None,
|
||||
"button_3_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"button_3_release": Light.RELEASE,
|
||||
"button_4_single": Light.CLICK_BRIGHTNESS_UP,
|
||||
"button_4_double": Light.ON_FULL_BRIGHTNESS,
|
||||
"button_4_triple": None,
|
||||
"button_4_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"button_4_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_single": Light.OFF,
|
||||
"1_double": Light.ON_MIN_COLOR_TEMP,
|
||||
"1_triple": None,
|
||||
"1_long press": Light.HOLD_COLOR_DOWN,
|
||||
"1_release": Light.RELEASE,
|
||||
"2_single": Light.ON,
|
||||
"2_double": Light.ON_FULL_COLOR_TEMP,
|
||||
"2_triple": None,
|
||||
"2_long press": Light.HOLD_COLOR_UP,
|
||||
"2_release": Light.RELEASE,
|
||||
"3_single": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"3_double": Light.ON_MIN_BRIGHTNESS,
|
||||
"3_triple": None,
|
||||
"3_long press": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"3_release": Light.RELEASE,
|
||||
"4_single": Light.CLICK_BRIGHTNESS_UP,
|
||||
"4_double": Light.ON_FULL_BRIGHTNESS,
|
||||
"4_triple": None,
|
||||
"4_long press": Light.HOLD_BRIGHTNESS_UP,
|
||||
"4_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class WXCJKG12LMZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_1_single": Z2MLight.OFF,
|
||||
"button_1_double": Z2MLight.ON_MIN_COLOR_TEMP,
|
||||
"button_1_triple": None,
|
||||
"button_1_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"button_1_release": Z2MLight.RELEASE,
|
||||
"button_2_single": Z2MLight.ON,
|
||||
"button_2_double": Z2MLight.ON_FULL_COLOR_TEMP,
|
||||
"button_2_triple": None,
|
||||
"button_2_hold": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"button_2_release": Z2MLight.RELEASE,
|
||||
"button_3_single": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"button_3_double": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
"button_3_triple": None,
|
||||
"button_3_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"button_3_release": Z2MLight.RELEASE,
|
||||
"button_4_single": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"button_4_double": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"button_4_triple": None,
|
||||
"button_4_hold": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"button_4_release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class WXCJKG13LMLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_1_single": Light.OFF,
|
||||
"button_1_double": Light.SYNC,
|
||||
"button_1_triple": None,
|
||||
"button_1_hold": None,
|
||||
"button_1_release": None,
|
||||
"button_2_single": Light.ON,
|
||||
"button_2_double": Light.SYNC,
|
||||
"button_2_triple": None,
|
||||
"button_2_hold": None,
|
||||
"button_2_release": None,
|
||||
"button_3_single": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"button_3_double": Light.ON_MIN_BRIGHTNESS,
|
||||
"button_3_triple": None,
|
||||
"button_3_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"button_3_release": Light.RELEASE,
|
||||
"button_4_single": Light.CLICK_BRIGHTNESS_UP,
|
||||
"button_4_double": Light.ON_FULL_BRIGHTNESS,
|
||||
"button_4_triple": None,
|
||||
"button_4_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"button_4_release": Light.RELEASE,
|
||||
"button_5_single": Light.CLICK_COLOR_DOWN,
|
||||
"button_5_double": Light.ON_MIN_COLOR_TEMP,
|
||||
"button_5_triple": None,
|
||||
"button_5_hold": Light.HOLD_COLOR_DOWN,
|
||||
"button_5_release": Light.RELEASE,
|
||||
"button_6_single": Light.CLICK_COLOR_UP,
|
||||
"button_6_double": Light.ON_FULL_COLOR_TEMP,
|
||||
"button_6_triple": None,
|
||||
"button_6_hold": Light.HOLD_COLOR_UP,
|
||||
"button_6_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.OFF,
|
||||
1004: Light.SYNC,
|
||||
1005: None,
|
||||
1001: None,
|
||||
1003: None,
|
||||
2002: Light.ON,
|
||||
2004: Light.SYNC,
|
||||
2005: None,
|
||||
2001: None,
|
||||
2003: None,
|
||||
3002: Light.CLICK_BRIGHTNESS_DOWN,
|
||||
3004: Light.ON_MIN_BRIGHTNESS,
|
||||
3005: None,
|
||||
3001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
3003: Light.RELEASE,
|
||||
4002: Light.CLICK_BRIGHTNESS_UP,
|
||||
4004: Light.ON_FULL_BRIGHTNESS,
|
||||
4005: None,
|
||||
4001: Light.HOLD_BRIGHTNESS_UP,
|
||||
4003: Light.RELEASE,
|
||||
5002: Light.CLICK_COLOR_DOWN,
|
||||
5004: Light.ON_MIN_COLOR_TEMP,
|
||||
5005: None,
|
||||
5001: Light.HOLD_COLOR_DOWN,
|
||||
5003: Light.RELEASE,
|
||||
6002: Light.CLICK_COLOR_UP,
|
||||
6004: Light.ON_FULL_COLOR_TEMP,
|
||||
6005: None,
|
||||
6001: Light.HOLD_COLOR_UP,
|
||||
6003: Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_single": Light.OFF,
|
||||
"1_double": Light.SYNC,
|
||||
"1_triple": None,
|
||||
"1_long": None,
|
||||
"1_release": None,
|
||||
"2_single": Light.ON,
|
||||
"2_double": Light.SYNC,
|
||||
"2_triple": None,
|
||||
"2_long": None,
|
||||
"2_release": None,
|
||||
"3_single": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"3_double": Light.ON_MIN_BRIGHTNESS,
|
||||
"3_triple": None,
|
||||
"3_long": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"3_release": Light.RELEASE,
|
||||
"4_single": Light.CLICK_BRIGHTNESS_UP,
|
||||
"4_double": Light.ON_FULL_BRIGHTNESS,
|
||||
"4_triple": None,
|
||||
"4_long": Light.HOLD_BRIGHTNESS_UP,
|
||||
"4_release": Light.RELEASE,
|
||||
"5_single": Light.CLICK_COLOR_DOWN,
|
||||
"5_double": Light.ON_MIN_COLOR_TEMP,
|
||||
"5_triple": None,
|
||||
"5_long": Light.HOLD_COLOR_DOWN,
|
||||
"5_release": Light.RELEASE,
|
||||
"6_single": Light.CLICK_COLOR_UP,
|
||||
"6_double": Light.ON_FULL_COLOR_TEMP,
|
||||
"6_triple": None,
|
||||
"6_long": Light.HOLD_COLOR_UP,
|
||||
"6_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class WXCJKG13LMZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_1_single": Z2MLight.OFF,
|
||||
"button_1_double": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"button_1_triple": None,
|
||||
"button_1_hold": None,
|
||||
"button_1_release": None,
|
||||
"button_2_single": Z2MLight.ON,
|
||||
"button_2_double": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"button_2_triple": None,
|
||||
"button_2_hold": None,
|
||||
"button_2_release": None,
|
||||
"button_3_single": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"button_3_double": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
"button_3_triple": None,
|
||||
"button_3_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"button_3_release": Z2MLight.RELEASE,
|
||||
"button_4_single": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"button_4_double": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"button_4_triple": None,
|
||||
"button_4_hold": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"button_4_release": Z2MLight.RELEASE,
|
||||
"button_5_single": Z2MLight.CLICK_COLOR_TEMP_DOWN,
|
||||
"button_5_double": Z2MLight.ON_MIN_COLOR_TEMP,
|
||||
"button_5_triple": None,
|
||||
"button_5_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"button_5_release": Z2MLight.RELEASE,
|
||||
"button_6_single": Z2MLight.CLICK_COLOR_TEMP_UP,
|
||||
"button_6_double": Z2MLight.ON_FULL_COLOR_TEMP,
|
||||
"button_6_triple": None,
|
||||
"button_6_hold": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"button_6_release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class WXCJKG13LMMediaPlayerController(MediaPlayerController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_1_single": MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
"button_1_double": MediaPlayer.MUTE,
|
||||
"button_1_triple": None,
|
||||
"button_1_hold": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"button_1_release": MediaPlayer.RELEASE,
|
||||
"button_2_single": MediaPlayer.CLICK_VOLUME_UP,
|
||||
"button_2_double": MediaPlayer.PLAY_PAUSE,
|
||||
"button_2_triple": None,
|
||||
"button_2_hold": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"button_2_release": MediaPlayer.RELEASE,
|
||||
"button_3_single": MediaPlayer.PREVIOUS_TRACK,
|
||||
"button_3_double": None,
|
||||
"button_3_triple": None,
|
||||
"button_3_hold": None,
|
||||
"button_3_release": None,
|
||||
"button_4_single": MediaPlayer.NEXT_TRACK,
|
||||
"button_4_double": None,
|
||||
"button_4_triple": None,
|
||||
"button_4_hold": None,
|
||||
"button_4_release": None,
|
||||
"button_5_single": MediaPlayer.PREVIOUS_SOURCE,
|
||||
"button_5_double": None,
|
||||
"button_5_triple": None,
|
||||
"button_5_hold": None,
|
||||
"button_5_release": None,
|
||||
"button_6_single": MediaPlayer.NEXT_SOURCE,
|
||||
"button_6_double": None,
|
||||
"button_6_triple": None,
|
||||
"button_6_hold": None,
|
||||
"button_6_release": None,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
#######################
|
||||
# Button layout
|
||||
#######################
|
||||
# 1000: top-left
|
||||
# 2000: top-right
|
||||
# 3000: middle-left
|
||||
# 4000: middle-right
|
||||
# 5000: bottom-left
|
||||
# 6000: bottom-right
|
||||
#######################
|
||||
# Actions
|
||||
#######################
|
||||
# 0001: hold
|
||||
# 0002: click
|
||||
# 0003: release
|
||||
# 0004: double-click
|
||||
# 0005: triple-click
|
||||
#######################
|
||||
return {
|
||||
1002: MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
1001: MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
1003: MediaPlayer.RELEASE,
|
||||
1004: MediaPlayer.MUTE,
|
||||
1005: None,
|
||||
2002: MediaPlayer.CLICK_VOLUME_UP,
|
||||
2001: MediaPlayer.HOLD_VOLUME_UP,
|
||||
2003: MediaPlayer.RELEASE,
|
||||
2004: MediaPlayer.PLAY_PAUSE,
|
||||
2005: None,
|
||||
3002: MediaPlayer.PREVIOUS_TRACK,
|
||||
3001: None,
|
||||
3003: None,
|
||||
3004: None,
|
||||
3005: None,
|
||||
4002: MediaPlayer.NEXT_TRACK,
|
||||
4001: None,
|
||||
4003: None,
|
||||
4004: None,
|
||||
4005: None,
|
||||
5002: MediaPlayer.PREVIOUS_SOURCE,
|
||||
5001: None,
|
||||
5003: None,
|
||||
5004: None,
|
||||
5005: None,
|
||||
6002: MediaPlayer.NEXT_SOURCE,
|
||||
6001: None,
|
||||
6003: None,
|
||||
6004: None,
|
||||
6005: None,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_single": MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
"1_double": MediaPlayer.MUTE,
|
||||
"1_triple": None,
|
||||
"1_long": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"1_release": MediaPlayer.RELEASE,
|
||||
"2_single": MediaPlayer.CLICK_VOLUME_UP,
|
||||
"2_double": MediaPlayer.PLAY_PAUSE,
|
||||
"2_triple": None,
|
||||
"2_long": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"2_release": MediaPlayer.RELEASE,
|
||||
"3_single": MediaPlayer.PREVIOUS_TRACK,
|
||||
"3_double": None,
|
||||
"3_triple": None,
|
||||
"3_long": None,
|
||||
"3_release": None,
|
||||
"4_single": MediaPlayer.NEXT_TRACK,
|
||||
"4_double": None,
|
||||
"4_triple": None,
|
||||
"4_long": None,
|
||||
"4_release": None,
|
||||
"5_single": MediaPlayer.PREVIOUS_SOURCE,
|
||||
"5_double": None,
|
||||
"5_triple": None,
|
||||
"5_long": None,
|
||||
"5_release": Light.RELEASE,
|
||||
"6_single": MediaPlayer.NEXT_SOURCE,
|
||||
"6_double": None,
|
||||
"6_triple": None,
|
||||
"6_long": None,
|
||||
"6_release": None,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class WXKG06LMLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"hold": Light.CLICK_BRIGHTNESS_UP,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE,
|
||||
1001: Light.CLICK_BRIGHTNESS_DOWN,
|
||||
1004: Light.CLICK_BRIGHTNESS_UP,
|
||||
}
|
||||
|
||||
|
||||
class WXKG06LMSwitchController(SwitchController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {"single": Light.TOGGLE}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {1002: Light.TOGGLE}
|
||||
|
||||
|
||||
class WXKG07LMLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_both": Light.TOGGLE,
|
||||
"double_both": Light.CLICK_BRIGHTNESS_UP,
|
||||
"hold_both": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"single_left": Light.TOGGLE,
|
||||
"double_left": Light.CLICK_BRIGHTNESS_UP,
|
||||
"hold_left": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"single_right": Light.TOGGLE,
|
||||
"double_right": Light.CLICK_BRIGHTNESS_UP,
|
||||
"hold_right": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1001: Light.CLICK_BRIGHTNESS_DOWN, # long left
|
||||
1002: Light.TOGGLE, # single left
|
||||
1004: Light.CLICK_BRIGHTNESS_UP, # double left
|
||||
2001: Light.CLICK_BRIGHTNESS_DOWN, # long right
|
||||
2002: Light.TOGGLE, # single right
|
||||
2004: Light.CLICK_BRIGHTNESS_UP, # double right
|
||||
3001: Light.CLICK_BRIGHTNESS_DOWN, # long both
|
||||
3002: Light.TOGGLE, # single both
|
||||
3004: Light.CLICK_BRIGHTNESS_UP, # double both
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"both_single": Light.TOGGLE,
|
||||
"both_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"both_long press": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"left_single": Light.TOGGLE,
|
||||
"left_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"left_long press": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"right_single": Light.TOGGLE,
|
||||
"right_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"right_long press": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class WXKG07LMZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_both": Z2MLight.TOGGLE,
|
||||
"double_both": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"hold_both": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"single_left": Z2MLight.TOGGLE,
|
||||
"double_left": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"hold_left": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"single_right": Z2MLight.TOGGLE,
|
||||
"double_right": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"hold_right": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
|
||||
class WXKG07LMSwitchController(SwitchController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_both": Switch.TOGGLE,
|
||||
"single_left": Switch.TOGGLE,
|
||||
"single_right": Switch.TOGGLE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Switch.TOGGLE,
|
||||
2002: Switch.TOGGLE,
|
||||
3002: Switch.TOGGLE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"both_single": Switch.TOGGLE,
|
||||
"left_single": Switch.TOGGLE,
|
||||
"right_single": Switch.TOGGLE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class ZNXNKG02LMLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
"hold": Light.HOLD_COLOR_TOGGLE,
|
||||
"release": Light.RELEASE,
|
||||
"start_rotating": Light.BRIGHTNESS_FROM_CONTROLLER_ANGLE,
|
||||
"stop_rotating": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class ZNXNKG02LMMediaPlayerController(MediaPlayerController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": MediaPlayer.PLAY_PAUSE,
|
||||
"double": MediaPlayer.NEXT_TRACK,
|
||||
"hold": MediaPlayer.PREVIOUS_TRACK,
|
||||
"release": MediaPlayer.RELEASE,
|
||||
"start_rotating": MediaPlayer.VOLUME_FROM_CONTROLLER_ANGLE,
|
||||
"stop_rotating": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class WXKG15LMLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_both": Light.TOGGLE,
|
||||
"double_both": Light.CLICK_BRIGHTNESS_UP,
|
||||
"triple_both": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"hold_both": Light.ON_FULL_BRIGHTNESS,
|
||||
"single_left": Light.TOGGLE,
|
||||
"double_left": Light.CLICK_BRIGHTNESS_UP,
|
||||
"triple_left": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"hold_left": Light.ON_FULL_BRIGHTNESS,
|
||||
"single_right": Light.TOGGLE,
|
||||
"double_right": Light.CLICK_BRIGHTNESS_UP,
|
||||
"triple_right": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"hold_right": Light.ON_FULL_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class WXKG15LMZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_both": Z2MLight.TOGGLE,
|
||||
"double_both": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"triple_both": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"hold_both": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"single_left": Z2MLight.TOGGLE,
|
||||
"double_left": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"triple_left": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"hold_left": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"single_right": Z2MLight.TOGGLE,
|
||||
"double_right": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"triple_right": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"hold_right": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class WXKG15LMSwitchController(SwitchController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_both": Switch.TOGGLE,
|
||||
"single_left": Switch.TOGGLE,
|
||||
"single_right": Switch.TOGGLE,
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
from cx_const import DefaultActionsMapping, Light
|
||||
from cx_core import LightController
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class AUA1ZBR2GWLightController(LightController):
|
||||
# Different states reported from the controller:
|
||||
# on, off, brightness_step_up, brightness_step_down,
|
||||
# color_temperature_step_up, color_temperature_step_down
|
||||
# There is one copy of these actions per endpoint
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_toggle": Light.TOGGLE,
|
||||
"1_step_up": Light.CLICK_BRIGHTNESS_UP,
|
||||
"1_step_down": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"1_step_color_temp_up": Light.CLICK_COLOR_TEMP_UP,
|
||||
"1_step_color_temp_down": Light.CLICK_COLOR_TEMP_DOWN,
|
||||
"2_toggle": Light.TOGGLE,
|
||||
"2_step_up": Light.CLICK_BRIGHTNESS_UP,
|
||||
"2_step_down": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"2_step_color_temp_up": Light.CLICK_COLOR_TEMP_UP,
|
||||
"2_step_color_temp_down": Light.CLICK_COLOR_TEMP_DOWN,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
endpoint_id = data.get("endpoint_id", 1)
|
||||
command: str = data["command"]
|
||||
action = command
|
||||
args = data.get("args", [])
|
||||
if command == "step" or command == "step_color_temp":
|
||||
args_mapping = {0: "up", 1: "down", 3: "up"}
|
||||
action = "_".join((action, args_mapping[args[0]]))
|
||||
if command == "on" or command == "off":
|
||||
action = "toggle"
|
||||
action = f"{endpoint_id}_{action}"
|
||||
return action
|
||||
@@ -0,0 +1,78 @@
|
||||
from cx_const import DefaultActionsMapping, Light
|
||||
from cx_core import LightController
|
||||
|
||||
|
||||
class HMPB2WM552LightController(LightController):
|
||||
def get_homematic_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"PRESS_SHORT_1": Light.OFF,
|
||||
"PRESS_LONG_1": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"PRESS_CONT_1": None,
|
||||
"PRESS_LONG_RELEASE_1": Light.RELEASE,
|
||||
"PRESS_SHORT_2": Light.ON,
|
||||
"PRESS_LONG_2": Light.HOLD_BRIGHTNESS_UP,
|
||||
"PRESS_CONT_2": None,
|
||||
"PRESS_LONG_RELEASE_2": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class HMPBI4FMLightController(LightController):
|
||||
def get_homematic_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"PRESS_SHORT_1": Light.OFF,
|
||||
"PRESS_CONT_1": None,
|
||||
"PRESS_LONG_1": Light.CLICK_COLOR_DOWN,
|
||||
"PRESS_SHORT_2": Light.ON,
|
||||
"PRESS_CONT_2": None,
|
||||
"PRESS_LONG_2": Light.CLICK_COLOR_UP,
|
||||
"PRESS_SHORT_3": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"PRESS_CONT_3": None,
|
||||
"PRESS_LONG_3": Light.ON_MIN_BRIGHTNESS,
|
||||
"PRESS_SHORT_4": Light.CLICK_BRIGHTNESS_UP,
|
||||
"PRESS_CONT_4": None,
|
||||
"PRESS_LONG_4": Light.ON_FULL_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class HMPB6WM55LightController(LightController):
|
||||
def get_homematic_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"PRESS_SHORT_1": Light.OFF,
|
||||
"PRESS_CONT_1": None,
|
||||
"PRESS_LONG_1": Light.SYNC,
|
||||
"PRESS_LONG_RELEASE_1": Light.RELEASE,
|
||||
"PRESS_SHORT_2": Light.ON,
|
||||
"PRESS_CONT_2": None,
|
||||
"PRESS_LONG_2": Light.SYNC,
|
||||
"PRESS_LONG_RELEASE_2": Light.RELEASE,
|
||||
"PRESS_SHORT_3": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"PRESS_CONT_3": None,
|
||||
"PRESS_LONG_3": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"PRESS_LONG_RELEASE_3": Light.RELEASE,
|
||||
"PRESS_SHORT_4": Light.CLICK_BRIGHTNESS_UP,
|
||||
"PRESS_CONT_4": None,
|
||||
"PRESS_LONG_4": Light.HOLD_BRIGHTNESS_UP,
|
||||
"PRESS_LONG_RELEASE_4": Light.RELEASE,
|
||||
"PRESS_SHORT_5": Light.CLICK_COLOR_DOWN,
|
||||
"PRESS_CONT_5": None,
|
||||
"PRESS_LONG_5": Light.HOLD_COLOR_DOWN,
|
||||
"PRESS_LONG_RELEASE_5": Light.RELEASE,
|
||||
"PRESS_SHORT_6": Light.CLICK_COLOR_UP,
|
||||
"PRESS_CONT_6": None,
|
||||
"PRESS_LONG_6": Light.HOLD_COLOR_UP,
|
||||
"PRESS_LONG_RELEASE_6": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class HMSenMDIRWM55LightController(LightController):
|
||||
def get_homematic_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"PRESS_SHORT_1": Light.OFF,
|
||||
"PRESS_LONG_1": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"PRESS_CONT_1": None,
|
||||
"PRESS_LONG_RELEASE_1": Light.RELEASE,
|
||||
"PRESS_SHORT_2": Light.ON,
|
||||
"PRESS_LONG_2": Light.HOLD_BRIGHTNESS_UP,
|
||||
"PRESS_CONT_2": None,
|
||||
"PRESS_LONG_RELEASE_2": Light.RELEASE,
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
from cx_const import (
|
||||
Cover,
|
||||
DefaultActionsMapping,
|
||||
Light,
|
||||
MediaPlayer,
|
||||
PredefinedActionsMapping,
|
||||
Switch,
|
||||
Z2MLight,
|
||||
)
|
||||
from cx_core import (
|
||||
CoverController,
|
||||
LightController,
|
||||
MediaPlayerController,
|
||||
SwitchController,
|
||||
Z2MLightController,
|
||||
action,
|
||||
)
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class E1810Controller(LightController):
|
||||
# Different states reported from the controller:
|
||||
# toggle, brightness_up_click, brightness_down_click
|
||||
# arrow_left_click, arrow_right_click, brightness_up_hold
|
||||
# brightness_up_release, brightness_down_hold, brightness_down_release,
|
||||
# arrow_left_hold, arrow_left_release, arrow_right_hold
|
||||
# arrow_right_release
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"toggle": Light.TOGGLE,
|
||||
"toggle_hold": Light.SYNC,
|
||||
"brightness_up_click": Light.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_down_click": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"arrow_left_click": Light.CLICK_COLOR_DOWN,
|
||||
"arrow_right_click": Light.CLICK_COLOR_UP,
|
||||
"brightness_up_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_up_release": Light.RELEASE,
|
||||
"brightness_down_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_down_release": Light.RELEASE,
|
||||
"arrow_left_hold": Light.HOLD_COLOR_DOWN,
|
||||
"arrow_left_release": Light.RELEASE,
|
||||
"arrow_right_hold": Light.HOLD_COLOR_UP,
|
||||
"arrow_right_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE,
|
||||
1001: Light.SYNC,
|
||||
2002: Light.CLICK_BRIGHTNESS_UP,
|
||||
3002: Light.CLICK_BRIGHTNESS_DOWN,
|
||||
4002: Light.CLICK_COLOR_DOWN,
|
||||
5002: Light.CLICK_COLOR_UP,
|
||||
2001: Light.HOLD_BRIGHTNESS_UP,
|
||||
2003: Light.RELEASE,
|
||||
3001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
3003: Light.RELEASE,
|
||||
4001: Light.HOLD_COLOR_DOWN,
|
||||
4003: Light.RELEASE,
|
||||
5001: Light.HOLD_COLOR_UP,
|
||||
5003: Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"toggle": Light.TOGGLE,
|
||||
"press_2_0_0": Light.SYNC,
|
||||
"step_with_on_off_0_43_5": Light.CLICK_BRIGHTNESS_UP,
|
||||
"step_1_43_5_0_0": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"press_257_13_0": Light.CLICK_COLOR_DOWN,
|
||||
"press_256_13_0": Light.CLICK_COLOR_UP,
|
||||
"move_with_on_off_0_83": Light.HOLD_BRIGHTNESS_UP,
|
||||
"move_with_on_off_0_84": Light.HOLD_BRIGHTNESS_UP, # ZigBee 3.0 firmware
|
||||
"move_1_83_0_0": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"move_1_84_0_0": Light.HOLD_BRIGHTNESS_DOWN, # ZigBee 3.0 firmware
|
||||
"hold_3329_0": Light.HOLD_COLOR_DOWN,
|
||||
"hold_3328_0": Light.HOLD_COLOR_UP,
|
||||
"stop": Light.RELEASE,
|
||||
"stop_with_on_off": Light.RELEASE,
|
||||
"release": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class E1810Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"toggle": Z2MLight.TOGGLE,
|
||||
"toggle_hold": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"brightness_up_click": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_down_click": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"arrow_left_click": Z2MLight.CLICK_COLOR_TEMP_DOWN,
|
||||
"arrow_right_click": Z2MLight.CLICK_COLOR_TEMP_UP,
|
||||
"brightness_up_hold": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_up_release": Z2MLight.RELEASE,
|
||||
"brightness_down_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_down_release": Z2MLight.RELEASE,
|
||||
"arrow_left_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"arrow_left_release": Z2MLight.RELEASE,
|
||||
"arrow_right_hold": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"arrow_right_release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class E1810MediaPlayerController(MediaPlayerController):
|
||||
# Different states reported from the controller:
|
||||
# toggle, brightness_up_click, brightness_down_click
|
||||
# arrow_left_click, arrow_right_click, brightness_up_hold
|
||||
# brightness_up_release, brightness_down_hold, brightness_down_release,
|
||||
# arrow_left_hold, arrow_left_release, arrow_right_hold
|
||||
# arrow_right_release
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"toggle": MediaPlayer.PLAY_PAUSE,
|
||||
"brightness_up_click": MediaPlayer.CLICK_VOLUME_UP,
|
||||
"brightness_down_click": MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
"arrow_left_click": MediaPlayer.PREVIOUS_TRACK,
|
||||
"arrow_right_click": MediaPlayer.NEXT_TRACK,
|
||||
"arrow_left_hold": MediaPlayer.PREVIOUS_SOURCE,
|
||||
"arrow_right_hold": MediaPlayer.NEXT_SOURCE,
|
||||
"brightness_up_hold": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"brightness_up_release": MediaPlayer.RELEASE,
|
||||
"brightness_down_hold": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"brightness_down_release": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: MediaPlayer.PLAY_PAUSE,
|
||||
2002: MediaPlayer.CLICK_VOLUME_UP,
|
||||
3002: MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
4002: MediaPlayer.PREVIOUS_TRACK,
|
||||
5002: MediaPlayer.NEXT_TRACK,
|
||||
2001: MediaPlayer.HOLD_VOLUME_UP,
|
||||
2003: MediaPlayer.RELEASE,
|
||||
3001: MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
3003: MediaPlayer.RELEASE,
|
||||
4001: MediaPlayer.PREVIOUS_SOURCE,
|
||||
5001: MediaPlayer.NEXT_SOURCE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"toggle": MediaPlayer.PLAY_PAUSE,
|
||||
"step_with_on_off_0_43_5": MediaPlayer.CLICK_VOLUME_UP,
|
||||
"step_1_43_5_0_0": MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
"press_257_13_0": MediaPlayer.PREVIOUS_TRACK,
|
||||
"press_256_13_0": MediaPlayer.NEXT_TRACK,
|
||||
"move_with_on_off_0_83": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"move_with_on_off_0_84": MediaPlayer.HOLD_VOLUME_UP, # ZigBee 3.0 firmware
|
||||
"stop": MediaPlayer.RELEASE,
|
||||
"stop_with_on_off": MediaPlayer.RELEASE,
|
||||
"move_1_83_0_0": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"move_1_84_0_0": MediaPlayer.HOLD_VOLUME_DOWN, # ZigBee 3.0 firmware
|
||||
"hold_3329_0": MediaPlayer.PREVIOUS_SOURCE,
|
||||
"hold_3328_0": MediaPlayer.NEXT_SOURCE,
|
||||
"release": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class E1743Controller(LightController):
|
||||
# Different states reported from the controller:
|
||||
# on, off, brightness_up, brightness_down, brightness_stop
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"off": Light.OFF,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.ON,
|
||||
2002: Light.OFF,
|
||||
1001: Light.HOLD_BRIGHTNESS_UP,
|
||||
2001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
1003: Light.RELEASE,
|
||||
2003: Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"off": Light.OFF,
|
||||
"move_with_on_off_0_83": Light.HOLD_BRIGHTNESS_UP,
|
||||
"move_1_83_0_0": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"stop": Light.RELEASE,
|
||||
"stop_with_on_off": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class E1743Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.ON,
|
||||
"off": Z2MLight.OFF,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class E1743MediaPlayerController(MediaPlayerController):
|
||||
# Different states reported from the controller:
|
||||
# on, off, brightness_up, brightness_down, brightness_stop
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": MediaPlayer.PLAY_PAUSE,
|
||||
"off": MediaPlayer.NEXT_TRACK,
|
||||
"brightness_move_up": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"brightness_move_down": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"brightness_stop": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: MediaPlayer.PLAY_PAUSE,
|
||||
2002: MediaPlayer.NEXT_TRACK,
|
||||
1001: MediaPlayer.HOLD_VOLUME_UP,
|
||||
2001: MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
1003: MediaPlayer.RELEASE,
|
||||
2003: MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": MediaPlayer.PLAY_PAUSE,
|
||||
"off": MediaPlayer.NEXT_TRACK,
|
||||
"move_with_on_off_0_83": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"move_1_83_0_0": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"stop": MediaPlayer.RELEASE,
|
||||
"stop_with_on_off": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class E1743SwitchController(SwitchController):
|
||||
# Different states reported from the controller:
|
||||
# on, off
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {"on": Switch.ON, "off": Switch.OFF}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {1002: Switch.ON, 2002: Switch.OFF}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {"on": Switch.ON, "off": Switch.OFF}
|
||||
|
||||
|
||||
class E1743CoverController(CoverController):
|
||||
# Different states reported from the controller:
|
||||
# on, off
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Cover.TOGGLE_OPEN,
|
||||
"off": Cover.TOGGLE_CLOSE,
|
||||
"brightness_move_up": Cover.OPEN,
|
||||
"brightness_move_down": Cover.CLOSE,
|
||||
"brightness_stop": Cover.STOP,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Cover.TOGGLE_OPEN,
|
||||
2002: Cover.TOGGLE_CLOSE,
|
||||
1001: Cover.OPEN,
|
||||
2001: Cover.CLOSE,
|
||||
1003: Cover.STOP,
|
||||
2003: Cover.STOP,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Cover.TOGGLE_OPEN,
|
||||
"off": Cover.TOGGLE_CLOSE,
|
||||
"move_with_on_off_0_83": Cover.OPEN,
|
||||
"move_1_83_0_0": Cover.CLOSE,
|
||||
"stop": Cover.STOP,
|
||||
"stop_with_on_off": Cover.STOP,
|
||||
}
|
||||
|
||||
|
||||
class E2201LightController(E1743Controller):
|
||||
pass
|
||||
|
||||
|
||||
class E2201Z2MLightController(E1743Z2MLightController):
|
||||
pass
|
||||
|
||||
|
||||
class E2201MediaPlayerController(E1743MediaPlayerController):
|
||||
pass
|
||||
|
||||
|
||||
class E2201SwitchController(E1743SwitchController):
|
||||
pass
|
||||
|
||||
|
||||
class E2201CoverController(E1743CoverController):
|
||||
pass
|
||||
|
||||
|
||||
class ICTCG1Controller(LightController):
|
||||
# Different states reported from the controller:
|
||||
# rotate_left, rotate_left_quick
|
||||
# rotate_right, rotate_right_quick
|
||||
# rotate_stop
|
||||
|
||||
@action
|
||||
async def rotate_left_quick(self) -> None:
|
||||
await self.release()
|
||||
await self.off()
|
||||
|
||||
@action
|
||||
async def rotate_right_quick(self) -> None:
|
||||
await self.release()
|
||||
await self.on_full(LightController.ATTRIBUTE_BRIGHTNESS)
|
||||
|
||||
def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
|
||||
parent_mapping = super().get_predefined_actions_mapping()
|
||||
mapping: PredefinedActionsMapping = {
|
||||
"rotate_left_quick": self.rotate_left_quick,
|
||||
"rotate_right_quick": self.rotate_right_quick,
|
||||
}
|
||||
mapping.update(parent_mapping)
|
||||
return mapping
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
"brightness_move_to_level": Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: "rotate_right_quick",
|
||||
2002: Light.CLICK_BRIGHTNESS_UP,
|
||||
3002: Light.CLICK_BRIGHTNESS_DOWN,
|
||||
4002: "rotate_left_quick",
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"move_1_70": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"move_1_195": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"move_to_level_with_on_off_0_1": "rotate_left_quick",
|
||||
"move_with_on_off_0_70": Light.HOLD_BRIGHTNESS_UP,
|
||||
"move_with_on_off_0_195": Light.ON,
|
||||
"move_to_level_with_on_off_255_1": "rotate_right_quick",
|
||||
"stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class ICTCG1Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
"brightness_move_to_level": Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL,
|
||||
}
|
||||
|
||||
|
||||
class ICTCG1MediaPlayerController(MediaPlayerController):
|
||||
# Different states reported from the controller:
|
||||
# rotate_left, rotate_left_quick
|
||||
# rotate_right, rotate_right_quick
|
||||
# rotate_stop
|
||||
|
||||
@action
|
||||
async def rotate_left_quick(self) -> None:
|
||||
await self.release()
|
||||
await self.pause()
|
||||
|
||||
@action
|
||||
async def rotate_right_quick(self) -> None:
|
||||
await self.release()
|
||||
await self.play()
|
||||
|
||||
def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
|
||||
parent_mapping = super().get_predefined_actions_mapping()
|
||||
mapping: PredefinedActionsMapping = {
|
||||
"rotate_left_quick": self.rotate_left_quick,
|
||||
"rotate_right_quick": self.rotate_right_quick,
|
||||
}
|
||||
mapping.update(parent_mapping)
|
||||
return mapping
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"brightness_move_up": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"brightness_move_down": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"brightness_stop": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: "rotate_right_quick",
|
||||
2002: MediaPlayer.CLICK_VOLUME_UP,
|
||||
3002: MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
4002: "rotate_left_quick",
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"move_1_70": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"move_1_195": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"move_to_level_with_on_off_0_1": "rotate_left_quick",
|
||||
"move_with_on_off_0_70": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"move_with_on_off_0_195": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"move_to_level_with_on_off_255_1": "rotate_right_quick",
|
||||
"stop": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class E1744LightController(LightController):
|
||||
# Different states reported from the controller:
|
||||
# brightness_move_down, brightness_move_up, brightness_stop,
|
||||
# toggle, brightness_step_up, brightness_step_down
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
"toggle": Light.TOGGLE,
|
||||
"brightness_step_up": Light.ON_FULL_BRIGHTNESS,
|
||||
"brightness_step_down": Light.ON_MIN_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
3001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
2001: Light.HOLD_BRIGHTNESS_UP,
|
||||
2003: Light.RELEASE,
|
||||
3003: Light.RELEASE,
|
||||
1002: Light.TOGGLE,
|
||||
1004: Light.ON_FULL_BRIGHTNESS,
|
||||
1005: Light.ON_MIN_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"move_1_195_0_0": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"move_0_195_0_0": Light.HOLD_BRIGHTNESS_UP,
|
||||
"stop": Light.RELEASE,
|
||||
"toggle": Light.TOGGLE,
|
||||
"step_0_1_0_0_0": Light.ON_FULL_BRIGHTNESS,
|
||||
"step_1_1_0_0_0": Light.ON_MIN_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def default_delay(self) -> int:
|
||||
return 500
|
||||
|
||||
|
||||
class E1744Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
"toggle": Z2MLight.TOGGLE,
|
||||
"brightness_step_up": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"brightness_step_down": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class E1744MediaPlayerController(MediaPlayerController):
|
||||
# Different states reported from the controller:
|
||||
# brightness_move_down, brightness_move_up, brightness_stop,
|
||||
# toggle, brightness_step_up, brightness_step_down
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"brightness_move_down": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"brightness_move_up": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"brightness_stop": MediaPlayer.RELEASE,
|
||||
"toggle": MediaPlayer.PLAY_PAUSE,
|
||||
"brightness_step_up": MediaPlayer.NEXT_TRACK,
|
||||
"brightness_step_down": MediaPlayer.PREVIOUS_TRACK,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
2001: MediaPlayer.HOLD_VOLUME_UP,
|
||||
3001: MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
2003: MediaPlayer.RELEASE,
|
||||
3003: MediaPlayer.RELEASE,
|
||||
1002: MediaPlayer.PLAY_PAUSE,
|
||||
1004: MediaPlayer.NEXT_TRACK,
|
||||
1005: MediaPlayer.PREVIOUS_TRACK,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"move_1_195_0_0": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"move_0_195_0_0": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"stop": MediaPlayer.RELEASE,
|
||||
"toggle": MediaPlayer.PLAY_PAUSE,
|
||||
"step_0_1_0_0_0": MediaPlayer.NEXT_TRACK,
|
||||
"step_1_1_0_0_0": MediaPlayer.PREVIOUS_TRACK,
|
||||
}
|
||||
|
||||
def default_delay(self) -> int:
|
||||
return 500
|
||||
|
||||
|
||||
class E1766LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {"open": Light.ON, "close": Light.OFF}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.ON,
|
||||
1003: Light.ON_FULL_BRIGHTNESS,
|
||||
2002: Light.OFF,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"up_open": Light.ON,
|
||||
"down_close": Light.OFF,
|
||||
}
|
||||
|
||||
|
||||
class E1766Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {"open": Z2MLight.ON, "close": Z2MLight.OFF}
|
||||
|
||||
|
||||
class E1766SwitchController(SwitchController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {"open": Switch.ON, "close": Switch.OFF}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {1002: Switch.ON, 2002: Switch.OFF}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {"up_open": Switch.ON, "down_close": Switch.OFF}
|
||||
|
||||
|
||||
class E1766CoverController(CoverController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"open": Cover.TOGGLE_OPEN,
|
||||
"close": Cover.TOGGLE_CLOSE,
|
||||
"stop": Cover.STOP,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Cover.TOGGLE_OPEN,
|
||||
1003: Cover.STOP,
|
||||
2002: Cover.TOGGLE_CLOSE,
|
||||
2003: Cover.STOP,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"up_open": Cover.TOGGLE_OPEN,
|
||||
"down_close": Cover.TOGGLE_CLOSE,
|
||||
"stop": Cover.STOP,
|
||||
}
|
||||
|
||||
|
||||
class E1812LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.TOGGLE,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE,
|
||||
1001: Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
1003: Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.TOGGLE,
|
||||
"move_with_on_off": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class E1812Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.TOGGLE,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class E1812SwitchController(SwitchController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {"on": Switch.TOGGLE}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {1002: Switch.TOGGLE}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {"on": Light.TOGGLE}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class E2002LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"off": Light.OFF,
|
||||
"arrow_left_click": Light.CLICK_COLOR_DOWN,
|
||||
"arrow_right_click": Light.CLICK_COLOR_UP,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"arrow_left_hold": Light.HOLD_COLOR_DOWN,
|
||||
"arrow_left_release": Light.RELEASE,
|
||||
"arrow_right_hold": Light.HOLD_COLOR_UP,
|
||||
"arrow_right_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.ON,
|
||||
2002: Light.OFF,
|
||||
3002: Light.CLICK_COLOR_DOWN,
|
||||
4002: Light.CLICK_COLOR_UP,
|
||||
1001: Light.HOLD_BRIGHTNESS_UP,
|
||||
1003: Light.RELEASE,
|
||||
2001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
2003: Light.RELEASE,
|
||||
3001: Light.HOLD_COLOR_DOWN,
|
||||
3003: Light.RELEASE,
|
||||
4001: Light.HOLD_COLOR_UP,
|
||||
4003: Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"off": Light.OFF,
|
||||
"press_257_13_0": Light.CLICK_COLOR_DOWN,
|
||||
"press_256_13_0": Light.CLICK_COLOR_UP,
|
||||
"move_with_on_off_0_83": Light.HOLD_BRIGHTNESS_UP,
|
||||
"move_1_83_0_0": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"hold_3329_0": Light.HOLD_COLOR_DOWN,
|
||||
"hold_3328_0": Light.HOLD_COLOR_UP,
|
||||
"stop": Light.RELEASE,
|
||||
"stop_with_on_off": Light.RELEASE,
|
||||
"release": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class W2049LightController(E2002LightController):
|
||||
async def initialize(self) -> None:
|
||||
await super().initialize()
|
||||
self.log(
|
||||
"⚠️ `W2049LightController` is deprecated and will be removed. Use `E2002LightController` instead.",
|
||||
level="WARNING",
|
||||
ascii_encode=False,
|
||||
)
|
||||
|
||||
|
||||
class E2002Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.ON,
|
||||
"off": Z2MLight.OFF,
|
||||
"arrow_left_click": Z2MLight.CLICK_COLOR_TEMP_DOWN,
|
||||
"arrow_right_click": Z2MLight.CLICK_COLOR_TEMP_UP,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"arrow_left_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"arrow_left_release": Z2MLight.RELEASE,
|
||||
"arrow_right_hold": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"arrow_right_release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class E2002MediaPlayerController(MediaPlayerController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": MediaPlayer.PLAY_PAUSE,
|
||||
"off": MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
"arrow_left_click": MediaPlayer.PREVIOUS_TRACK,
|
||||
"arrow_right_click": MediaPlayer.NEXT_TRACK,
|
||||
"arrow_left_hold": MediaPlayer.PREVIOUS_SOURCE,
|
||||
"arrow_left_release": MediaPlayer.RELEASE,
|
||||
"arrow_right_hold": MediaPlayer.NEXT_SOURCE,
|
||||
"arrow_right_release": MediaPlayer.RELEASE,
|
||||
"brightness_move_up": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"brightness_move_down": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"brightness_stop": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: MediaPlayer.PLAY_PAUSE,
|
||||
2002: MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
3002: MediaPlayer.PREVIOUS_TRACK,
|
||||
4002: MediaPlayer.NEXT_TRACK,
|
||||
1001: MediaPlayer.HOLD_VOLUME_UP,
|
||||
1003: MediaPlayer.RELEASE,
|
||||
2001: MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
2003: MediaPlayer.RELEASE,
|
||||
3001: MediaPlayer.PREVIOUS_SOURCE,
|
||||
3003: MediaPlayer.RELEASE,
|
||||
4001: MediaPlayer.NEXT_SOURCE,
|
||||
4003: MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": MediaPlayer.PLAY_PAUSE,
|
||||
"off": MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
"press_257_13_0": MediaPlayer.PREVIOUS_TRACK,
|
||||
"press_256_13_0": MediaPlayer.NEXT_TRACK,
|
||||
"move_with_on_off_0_83": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"move_1_83_0_0": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"hold_3329_0": MediaPlayer.PREVIOUS_SOURCE,
|
||||
"hold_3328_0": MediaPlayer.NEXT_SOURCE,
|
||||
"stop": MediaPlayer.RELEASE,
|
||||
"stop_with_on_off": MediaPlayer.RELEASE,
|
||||
"release": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class W2049MediaPlayerController(E2002MediaPlayerController):
|
||||
async def initialize(self) -> None:
|
||||
await super().initialize()
|
||||
self.log(
|
||||
"⚠️ `W2049MediaPlayerController` is deprecated and will be removed. "
|
||||
"Use `E2002MediaPlayerController` instead.",
|
||||
level="WARNING",
|
||||
ascii_encode=False,
|
||||
)
|
||||
|
||||
|
||||
class E2213LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_long_press": Light.HOLD_BRIGHTNESS_UP,
|
||||
"2_long_press": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"1_short_release": Light.ON,
|
||||
"2_short_release": Light.OFF,
|
||||
"1_long_release": Light.RELEASE,
|
||||
"2_long_release": Light.RELEASE,
|
||||
"1_double_press": Light.HOLD_COLOR_UP,
|
||||
"2_double_press": Light.HOLD_COLOR_DOWN,
|
||||
}
|
||||
|
||||
|
||||
class E2213Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_long_press": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"2_long_press": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"1_short_release": Z2MLight.ON,
|
||||
"2_short_release": Z2MLight.OFF,
|
||||
"1_long_release": Z2MLight.RELEASE,
|
||||
"2_long_release": Z2MLight.RELEASE,
|
||||
"1_double_press": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"2_double_press": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
}
|
||||
|
||||
|
||||
class E2123MediaPlayerController(MediaPlayerController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"toggle": MediaPlayer.PLAY_PAUSE, # click Play button
|
||||
"play_pause": MediaPlayer.PLAY_PAUSE, # click Play button
|
||||
"track_next": MediaPlayer.NEXT_TRACK, # click Next Track
|
||||
"track_previous": MediaPlayer.PREVIOUS_TRACK, # click Previous Track
|
||||
"volume_up": MediaPlayer.CLICK_VOLUME_UP, # click + (Volume up)
|
||||
"volume_up_hold": MediaPlayer.CLICK_VOLUME_UP, # hold + (Volume up)
|
||||
"volume_down": MediaPlayer.CLICK_VOLUME_DOWN, # click - (Volume down)
|
||||
"volume_down_hold": MediaPlayer.CLICK_VOLUME_DOWN, # hold - (Volume down)
|
||||
"dots_1_initial_press": MediaPlayer.PREVIOUS_SOURCE, # click . (Previous Source)
|
||||
"dots_2_initial_press": MediaPlayer.NEXT_SOURCE, # click .. (Next Source)
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: MediaPlayer.PLAY, # Play; Press/Release or Hold
|
||||
2001: MediaPlayer.HOLD_VOLUME_UP, # +; Hold (every 0.2s)
|
||||
2002: MediaPlayer.RELEASE, # +; Press/Release
|
||||
3001: MediaPlayer.HOLD_VOLUME_DOWN, # +; Hold (every 0.2s)
|
||||
3002: MediaPlayer.RELEASE, # -; Press/Release
|
||||
4002: MediaPlayer.PREVIOUS_TRACK, # Previous; Press/Release or Hold
|
||||
5002: MediaPlayer.NEXT_TRACK, # Next; Press/Release or Hold
|
||||
6000: MediaPlayer.PREVIOUS_SOURCE, # .; Initial Press
|
||||
7000: MediaPlayer.NEXT_SOURCE, # ..; Initial Press
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"toggle": MediaPlayer.PLAY_PAUSE, # click Play button
|
||||
"play_pause": MediaPlayer.PLAY_PAUSE, # click Play button
|
||||
"step_0_1_0_0_0": MediaPlayer.NEXT_TRACK, # click Next Track
|
||||
"step_1_1_0_0_0": MediaPlayer.PREVIOUS_TRACK, # click Previous Track
|
||||
"move_with_on_off_0_255": MediaPlayer.CLICK_VOLUME_UP, # click + (Volume up)
|
||||
"move_0_255_0_0": MediaPlayer.HOLD_VOLUME_UP, # hold + (Volume up)
|
||||
"move_with_on_off_1_255": MediaPlayer.CLICK_VOLUME_DOWN, # click - (Volume down)
|
||||
"move_1_255_0_0": MediaPlayer.HOLD_VOLUME_DOWN, # hold - (Volume down)
|
||||
"2_initial_press_0": MediaPlayer.PREVIOUS_SOURCE, # click . (Previous Source)
|
||||
"3_initial_press_0": MediaPlayer.NEXT_SOURCE, # click .. (Next Source)
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command = data["command"]
|
||||
args = data["args"]
|
||||
if isinstance(args, dict):
|
||||
args = args["args"]
|
||||
args = list(map(str, args))
|
||||
action = command
|
||||
if not (command == "stop" or command == "release"):
|
||||
if len(args) > 0:
|
||||
action += "_" + "_".join(args)
|
||||
endpoint = data["endpoint_id"]
|
||||
# We only need to add the endpoint ID for the dot buttons, which use 2 and 3
|
||||
if endpoint != 1:
|
||||
return f"{data['endpoint_id']}_{action}"
|
||||
else:
|
||||
return action
|
||||
@@ -0,0 +1,100 @@
|
||||
from typing import Any
|
||||
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
def get_zha_action_LegrandWallController(data: dict[str, Any]) -> str:
|
||||
endpoint_id = data.get("endpoint_id", 1)
|
||||
command: str = data["command"]
|
||||
action = command
|
||||
args = data.get("args", {})
|
||||
args_mapping = {0: "up", 1: "down"}
|
||||
if command == "move":
|
||||
action = "_".join((action, args_mapping[args[0]]))
|
||||
action = "_".join((str(endpoint_id), action))
|
||||
return action
|
||||
|
||||
|
||||
class Legrand600083LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"off": Light.OFF,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_on": Light.ON,
|
||||
"1_off": Light.OFF,
|
||||
"1_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"1_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"1_stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
return get_zha_action_LegrandWallController(data)
|
||||
|
||||
|
||||
class Legrand600083Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.ON,
|
||||
"off": Z2MLight.OFF,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class Legrand600088LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_left": Light.ON,
|
||||
"off_left": Light.OFF,
|
||||
"brightness_move_up_left": Light.HOLD_COLOR_UP,
|
||||
"brightness_move_down_left": Light.HOLD_COLOR_DOWN,
|
||||
"brightness_stop_left": Light.RELEASE,
|
||||
"on_right": Light.ON_FULL_BRIGHTNESS,
|
||||
"off_right": Light.ON_MIN_BRIGHTNESS,
|
||||
"brightness_move_up_right": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_right": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_right": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_on": Light.ON,
|
||||
"1_off": Light.OFF,
|
||||
"1_move_up": Light.HOLD_COLOR_UP,
|
||||
"1_move_down": Light.HOLD_COLOR_DOWN,
|
||||
"1_stop": Light.RELEASE,
|
||||
"2_on": Light.ON_FULL_BRIGHTNESS,
|
||||
"2_off": Light.ON_MIN_BRIGHTNESS,
|
||||
"2_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"2_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"2_stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
return get_zha_action_LegrandWallController(data)
|
||||
|
||||
|
||||
class Legrand600088Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_left": Z2MLight.ON,
|
||||
"off_left": Z2MLight.OFF,
|
||||
"brightness_move_up_left": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"brightness_move_down_left": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"brightness_stop_left": Z2MLight.RELEASE,
|
||||
"on_right": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"off_right": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
"brightness_move_up_right": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_right": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_right": Z2MLight.RELEASE,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
|
||||
|
||||
class ZS23000278LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"off": Light.OFF,
|
||||
"brightness_step_up": Light.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_step_down": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"brightness_move_to_level": Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
"color_temperature_move": Light.COLORTEMP_FROM_CONTROLLER,
|
||||
"color_temperature_move_up": Light.CLICK_COLOR_TEMP_UP,
|
||||
"color_temperature_move_down": Light.CLICK_COLOR_TEMP_DOWN,
|
||||
"color_move": Light.XYCOLOR_FROM_CONTROLLER,
|
||||
}
|
||||
|
||||
|
||||
class ZS23000278Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.ON,
|
||||
"off": Z2MLight.OFF,
|
||||
"brightness_step_up": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_step_down": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"brightness_move_to_level": Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
"color_temperature_move": Z2MLight.COLORTEMP_FROM_CONTROLLER,
|
||||
"color_temperature_move_up": Z2MLight.CLICK_COLOR_TEMP_UP,
|
||||
"color_temperature_move_down": Z2MLight.CLICK_COLOR_TEMP_DOWN,
|
||||
"color_move": Z2MLight.XYCOLOR_FROM_CONTROLLER,
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
|
||||
|
||||
class HG06323LightController(LightController):
|
||||
# Different states reported from the controller:
|
||||
# on, off, brightness_step_up, brightness_move_up,
|
||||
# brightness_step_down, brightness_move_down, brightness_stop
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"brightness_step_up": Light.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
"brightness_step_down": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"off": Light.OFF,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"step_0_51_10": Light.CLICK_BRIGHTNESS_UP,
|
||||
"move_0_51": Light.HOLD_BRIGHTNESS_UP,
|
||||
"stop": Light.RELEASE,
|
||||
"step_1_51_10": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"move_1_51": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"off": Light.OFF,
|
||||
}
|
||||
|
||||
|
||||
class HG06323Z2MLightController(Z2MLightController):
|
||||
# Different states reported from the controller:
|
||||
# on, off, brightness_step_up, brightness_move_up,
|
||||
# brightness_step_down, brightness_move_down, brightness_stop
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.ON,
|
||||
"brightness_step_up": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
"brightness_step_down": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"off": Z2MLight.OFF,
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
from cx_const import DefaultActionsMapping, Light, MediaPlayer, Z2MLight
|
||||
from cx_core import LightController, MediaPlayerController
|
||||
from cx_core.type.z2m_light_controller import Z2MLightController
|
||||
|
||||
|
||||
class LZL4BWHL01LightController(LightController):
|
||||
# Each button press fires an event but no separate
|
||||
# hold event. Press of up or down generates a stop event
|
||||
# when released.
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.ON_FULL_BRIGHTNESS,
|
||||
2001: Light.HOLD_BRIGHTNESS_UP,
|
||||
2003: Light.RELEASE,
|
||||
3001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
3003: Light.RELEASE,
|
||||
4002: Light.OFF,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"move_to_level_with_on_off_254_4": Light.ON_FULL_BRIGHTNESS,
|
||||
"step_with_on_off_0_30_6": Light.HOLD_BRIGHTNESS_UP,
|
||||
"step_1_30_6": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"move_to_level_with_on_off_0_4": Light.OFF,
|
||||
"stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class Z31BRLLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"brightness_move_to_level": Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE,
|
||||
2002: Light.CLICK_BRIGHTNESS_UP,
|
||||
3002: Light.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
|
||||
class Z31BRLZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"brightness_move_to_level": Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL,
|
||||
}
|
||||
|
||||
|
||||
class LutronPJ22BLightController(LightController):
|
||||
def get_state_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1": Light.ON_FULL_BRIGHTNESS,
|
||||
"4": Light.OFF,
|
||||
}
|
||||
|
||||
def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_2_press": Light.ON,
|
||||
"button_4_press": Light.OFF,
|
||||
}
|
||||
|
||||
|
||||
class LutronPJ22BMediaPlayerController(MediaPlayerController):
|
||||
def get_state_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1": MediaPlayer.PLAY_PAUSE,
|
||||
"4": MediaPlayer.NEXT_TRACK,
|
||||
}
|
||||
|
||||
def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_2_press": MediaPlayer.PLAY_PAUSE,
|
||||
"button_4_press": MediaPlayer.NEXT_TRACK,
|
||||
}
|
||||
|
||||
|
||||
class LutronPJ22BRLLightController(LightController):
|
||||
def get_state_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1": Light.ON_FULL_BRIGHTNESS,
|
||||
"8": Light.HOLD_BRIGHTNESS_UP,
|
||||
"16": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"4": Light.OFF,
|
||||
"0": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class LutronPJ22BRLMediaPlayerController(MediaPlayerController):
|
||||
def get_state_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1": MediaPlayer.PLAY_PAUSE,
|
||||
"8": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"16": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"4": MediaPlayer.NEXT_TRACK,
|
||||
"0": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class LutronPJ23BRLLightController(LightController):
|
||||
def get_state_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1": Light.ON_FULL_BRIGHTNESS,
|
||||
"8": Light.HOLD_BRIGHTNESS_UP,
|
||||
"2": Light.SET_HALF_BRIGHTNESS,
|
||||
"16": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"4": Light.OFF,
|
||||
"0": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_2_press": Light.ON_FULL_BRIGHTNESS,
|
||||
"button_4_press": Light.OFF,
|
||||
"button_3_press": Light.SET_HALF_BRIGHTNESS,
|
||||
"button_5_press": Light.HOLD_BRIGHTNESS_UP,
|
||||
"button_5_release": Light.RELEASE,
|
||||
"button_6_press": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"button_6_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class LutronPJ23BRLMediaPlayerController(MediaPlayerController):
|
||||
def get_state_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1": MediaPlayer.PLAY_PAUSE,
|
||||
"8": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"2": MediaPlayer.NEXT_SOURCE,
|
||||
"16": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"4": MediaPlayer.NEXT_TRACK,
|
||||
"0": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_2_press": MediaPlayer.PLAY_PAUSE,
|
||||
"button_4_press": MediaPlayer.NEXT_TRACK,
|
||||
"button_3_press": MediaPlayer.NEXT_SOURCE,
|
||||
"button_5_press": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"button_5_release": MediaPlayer.RELEASE,
|
||||
"button_6_press": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"button_6_release": MediaPlayer.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class LutronPJ24BLightController(LightController):
|
||||
def get_state_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1": Light.ON_FULL_BRIGHTNESS,
|
||||
"2": Light.HOLD_BRIGHTNESS_UP,
|
||||
"4": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"8": Light.OFF,
|
||||
"0": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class LutronPJ24BMediaPlayerController(MediaPlayerController):
|
||||
def get_state_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1": MediaPlayer.PLAY_PAUSE,
|
||||
"2": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"4": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"8": MediaPlayer.NEXT_TRACK,
|
||||
"0": MediaPlayer.RELEASE,
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
from cx_core.controller import Controller
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class MLI404011LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.TOGGLE,
|
||||
"off": Light.TOGGLE,
|
||||
"brightness_down_click": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"brightness_down_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_down_release": Light.RELEASE,
|
||||
"brightness_up_click": Light.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_up_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_up_release": Light.RELEASE,
|
||||
"color_wheel": Light.XYCOLOR_FROM_CONTROLLER, # Color ring press
|
||||
"color_temp": Light.COLORTEMP_FROM_CONTROLLER, # warm or cold
|
||||
"scene_1": None, # sunset button
|
||||
"scene_2": None, # party button
|
||||
"scene_3": None, # reading button
|
||||
"scene_4": None, # fire button
|
||||
"scene_5": None, # heart button
|
||||
"scene_6": None, # night button
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE,
|
||||
2001: Light.HOLD_BRIGHTNESS_UP,
|
||||
2002: Light.CLICK_BRIGHTNESS_UP,
|
||||
2003: Light.RELEASE,
|
||||
3001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
3002: Light.CLICK_BRIGHTNESS_DOWN,
|
||||
3003: Light.RELEASE,
|
||||
4002: Light.CLICK_COLOR_UP,
|
||||
5002: Light.CLICK_COLOR_DOWN,
|
||||
6002: Light.XYCOLOR_FROM_CONTROLLER, # Color ring press
|
||||
7002: None, # reading button
|
||||
8002: None, # sunset button
|
||||
9002: None, # party button
|
||||
10002: None, # night button
|
||||
11002: None, # fire button
|
||||
12002: None, # heart button
|
||||
}
|
||||
|
||||
|
||||
class MLI404011Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.TOGGLE,
|
||||
"off": Z2MLight.TOGGLE,
|
||||
"brightness_down_click": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"brightness_down_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_down_release": Z2MLight.RELEASE,
|
||||
"brightness_up_click": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_up_hold": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_up_release": Z2MLight.RELEASE,
|
||||
"color_wheel": Z2MLight.XYCOLOR_FROM_CONTROLLER, # Color ring press
|
||||
"color_temp": Z2MLight.COLORTEMP_FROM_CONTROLLER, # warm or cold
|
||||
"scene_1": None, # sunset button
|
||||
"scene_2": None, # party button
|
||||
"scene_3": None, # reading button
|
||||
"scene_4": None, # fire button
|
||||
"scene_5": None, # heart button
|
||||
"scene_6": None, # night button
|
||||
}
|
||||
|
||||
|
||||
class MLI404002Controller(Controller):
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
if command not in ("move", "step"):
|
||||
return command
|
||||
args = data["args"]
|
||||
direction_mapping = {0: "up", 1: "down"}
|
||||
return f"{command}_{direction_mapping[args[0]]}"
|
||||
|
||||
|
||||
class MLI404002LightController(MLI404002Controller, LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.TOGGLE,
|
||||
"off": Light.TOGGLE,
|
||||
"brightness_step_up": Light.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_step_down": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
"recall_1": Light.ON_FULL_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.TOGGLE,
|
||||
"off": Light.TOGGLE,
|
||||
"move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"stop": Light.RELEASE,
|
||||
"step_up": Light.CLICK_BRIGHTNESS_UP,
|
||||
"step_down": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"recall": Light.ON_FULL_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class MLI404002Z2MLightController(MLI404002Controller, Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.TOGGLE,
|
||||
"off": Z2MLight.TOGGLE,
|
||||
"brightness_step_up": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_step_down": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
"recall_1": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
|
||||
|
||||
class Namron4512773LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_l1": Light.ON,
|
||||
"off_l1": Light.OFF,
|
||||
"brightness_move_up_l1": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_l1": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_l1": Light.RELEASE,
|
||||
"on_l2": Light.ON,
|
||||
"off_l2": Light.OFF,
|
||||
"brightness_move_up_l2": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_l2": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_l2": Light.RELEASE,
|
||||
"on_l3": Light.ON,
|
||||
"off_l3": Light.OFF,
|
||||
"brightness_move_up_l3": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_l3": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_l3": Light.RELEASE,
|
||||
"on_l4": Light.ON,
|
||||
"off_l4": Light.OFF,
|
||||
"brightness_move_up_l4": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_l4": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_l4": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class Namron4512773Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_l1": Z2MLight.ON,
|
||||
"off_l1": Z2MLight.OFF,
|
||||
"brightness_move_up_l1": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_l1": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_l1": Z2MLight.RELEASE,
|
||||
"on_l2": Z2MLight.ON,
|
||||
"off_l2": Z2MLight.OFF,
|
||||
"brightness_move_up_l2": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_l2": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_l2": Z2MLight.RELEASE,
|
||||
"on_l3": Z2MLight.ON,
|
||||
"off_l3": Z2MLight.OFF,
|
||||
"brightness_move_up_l3": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_l3": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_l3": Z2MLight.RELEASE,
|
||||
"on_l4": Z2MLight.ON,
|
||||
"off_l4": Z2MLight.OFF,
|
||||
"brightness_move_up_l4": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_l4": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_l4": Z2MLight.RELEASE,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
from cx_const import DefaultActionsMapping, Light
|
||||
from cx_core import LightController
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class OsramAC025XX00NJLightController(LightController):
|
||||
# This mapping works for: AC0251100NJ / AC0251400NJ / AC0251600NJ / AC0251700NJ
|
||||
# (different models are just different colours)
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.ON, # Click Arrow up
|
||||
1001: Light.HOLD_BRIGHTNESS_UP, # Hold Arrow Up
|
||||
1003: Light.RELEASE, # Release Arrow Up
|
||||
2002: Light.OFF, # Click Arrow down
|
||||
2001: Light.HOLD_BRIGHTNESS_DOWN, # Hold Arrow down
|
||||
2003: Light.RELEASE, # Release Arrow down
|
||||
3002: Light.SYNC, # Click Circle button
|
||||
3001: Light.HOLD_COLOR_UP, # Hold Circle button
|
||||
3003: Light.RELEASE, # Release Circle button
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_on": Light.ON,
|
||||
"1_move_with_on_off": Light.HOLD_BRIGHTNESS_UP,
|
||||
"1_stop": Light.RELEASE,
|
||||
"3_move_to_level_with_on_off": None,
|
||||
"3_move_to_color_temp": Light.SYNC,
|
||||
"3_move_to_saturation": Light.HOLD_COLOR_UP,
|
||||
"3_move_hue": Light.RELEASE,
|
||||
"2_off": Light.OFF,
|
||||
"2_move": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"2_stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
return f"{data['endpoint_id']}_{data['command']}"
|
||||
@@ -0,0 +1,326 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class HueDimmerController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_press_release": Light.ON,
|
||||
"on_hold": Light.HOLD_COLOR_UP,
|
||||
"on_hold_release": Light.RELEASE,
|
||||
"up_press_release": Light.CLICK_BRIGHTNESS_UP,
|
||||
"up_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"up_hold_release": Light.RELEASE,
|
||||
"down_press_release": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"down_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"down_hold_release": Light.RELEASE,
|
||||
"off_press_release": Light.OFF,
|
||||
"off_hold": Light.HOLD_COLOR_DOWN,
|
||||
"off_hold_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.ON,
|
||||
1001: Light.HOLD_COLOR_UP,
|
||||
1003: Light.RELEASE,
|
||||
2002: Light.CLICK_BRIGHTNESS_UP,
|
||||
2001: Light.HOLD_BRIGHTNESS_UP,
|
||||
2003: Light.RELEASE,
|
||||
3002: Light.CLICK_BRIGHTNESS_DOWN,
|
||||
3001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
3003: Light.RELEASE,
|
||||
4002: Light.OFF,
|
||||
4001: Light.HOLD_COLOR_DOWN,
|
||||
4003: Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"off_long_release": Light.RELEASE,
|
||||
"off_hold": Light.HOLD_COLOR_DOWN,
|
||||
"off_short_release": Light.OFF,
|
||||
"down_long_release": Light.RELEASE,
|
||||
"down_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"down_short_release": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"up_long_release": Light.RELEASE,
|
||||
"up_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"up_short_release": Light.CLICK_BRIGHTNESS_UP,
|
||||
"on_long_release": Light.RELEASE,
|
||||
"on_hold": Light.HOLD_COLOR_UP,
|
||||
"on_short_release": Light.ON,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class HueDimmerZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_press_release": Z2MLight.ON,
|
||||
"on_hold": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"on_hold_release": Z2MLight.RELEASE,
|
||||
"up_press_release": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"up_hold": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"up_hold_release": Z2MLight.RELEASE,
|
||||
"down_press_release": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"down_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"down_hold_release": Z2MLight.RELEASE,
|
||||
"off_press_release": Z2MLight.OFF,
|
||||
"off_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"off_hold_release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class Philips929002398602LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_press_release": Light.TOGGLE,
|
||||
"on_hold": Light.TOGGLE,
|
||||
"on_hold_release": Light.RELEASE,
|
||||
"up_press_release": Light.CLICK_BRIGHTNESS_UP,
|
||||
"up_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"up_hold_release": Light.RELEASE,
|
||||
"down_press_release": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"down_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"down_hold_release": Light.RELEASE,
|
||||
"off_press_release": Light.CLICK_COLOR_UP,
|
||||
"off_hold": Light.HOLD_COLOR_DOWN,
|
||||
"off_hold_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE,
|
||||
1001: Light.TOGGLE,
|
||||
1003: Light.RELEASE,
|
||||
2002: Light.CLICK_BRIGHTNESS_UP,
|
||||
2001: Light.HOLD_BRIGHTNESS_UP,
|
||||
2003: Light.RELEASE,
|
||||
3002: Light.CLICK_BRIGHTNESS_DOWN,
|
||||
3001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
3003: Light.RELEASE,
|
||||
4002: Light.CLICK_COLOR_UP,
|
||||
4001: Light.HOLD_COLOR_DOWN,
|
||||
4003: Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"off_long_release": Light.RELEASE,
|
||||
"off_hold": Light.HOLD_COLOR_DOWN,
|
||||
"off_short_release": Light.CLICK_COLOR_UP,
|
||||
"down_long_release": Light.RELEASE,
|
||||
"down_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"down_short_release": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"up_long_release": Light.RELEASE,
|
||||
"up_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"up_short_release": Light.CLICK_BRIGHTNESS_UP,
|
||||
"on_long_release": Light.RELEASE,
|
||||
"on_hold": Light.TOGGLE,
|
||||
"on_short_release": Light.TOGGLE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class Philips929002398602Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_press_release": Z2MLight.ON,
|
||||
"on_hold": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"on_hold_release": Z2MLight.RELEASE,
|
||||
"up_press_release": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"up_hold": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"up_hold_release": Z2MLight.RELEASE,
|
||||
"down_press_release": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"down_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"down_hold_release": Z2MLight.RELEASE,
|
||||
"off_press_release": Z2MLight.OFF,
|
||||
"off_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"off_hold_release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class PTM215XLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"press_1": Light.ON,
|
||||
"release_1": Light.RELEASE,
|
||||
"press_2": Light.OFF,
|
||||
"release_2": Light.RELEASE,
|
||||
"press_3": Light.ON_FULL_BRIGHTNESS,
|
||||
"release_3": Light.RELEASE,
|
||||
"press_4": Light.ON_MIN_BRIGHTNESS,
|
||||
"release_4": Light.RELEASE,
|
||||
"press_1_and_3": Light.ON_FULL_COLOR_TEMP,
|
||||
"release_1_and_3": Light.RELEASE,
|
||||
"press_2_and_4": Light.ON_MIN_COLOR_TEMP,
|
||||
"release_2_and_4": Light.RELEASE,
|
||||
"press_energy_bar": Light.SYNC,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.ON, # button_1_release
|
||||
1001: Light.SYNC, # button_1_hold
|
||||
1000: None, # button_1_press
|
||||
1003: None, # button_1_release_after_hold
|
||||
2002: Light.OFF, # button_2_release
|
||||
2001: Light.SYNC, # button_2_hold
|
||||
2000: None, # button_2_press
|
||||
2003: None, # button_2_release_after_hold
|
||||
3002: Light.ON_FULL_BRIGHTNESS, # button_3_release
|
||||
3001: Light.HOLD_BRIGHTNESS_UP, # button_3_hold
|
||||
3000: None, # button_3_press
|
||||
3003: Light.RELEASE, # button_3_release_after_hold
|
||||
4002: Light.ON_MIN_BRIGHTNESS, # button_4_release
|
||||
4001: Light.HOLD_BRIGHTNESS_DOWN, # button_4_hold
|
||||
4000: None, # button_4_press
|
||||
4003: Light.RELEASE, # button_4_release_after_hold
|
||||
5002: Light.ON_FULL_COLOR_TEMP, # button_1_3_release
|
||||
5001: Light.HOLD_COLOR_UP, # button_1_3_hold
|
||||
5000: None, # button_1_3_press
|
||||
5003: Light.RELEASE, # button_1_3_release_after_hold
|
||||
6002: Light.ON_MIN_COLOR_TEMP, # button_2_4_release
|
||||
6001: Light.HOLD_COLOR_DOWN, # button_2_4_hold
|
||||
6000: None, # button_2_4_press
|
||||
6003: Light.RELEASE, # button_2_4_release_after_hold
|
||||
}
|
||||
|
||||
|
||||
class HueSmartButtonLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.TOGGLE,
|
||||
"off": Light.TOGGLE,
|
||||
"hold": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1000: None, # Initial press
|
||||
1001: Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
1002: Light.TOGGLE,
|
||||
1003: Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class HueSmartButtonZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.TOGGLE,
|
||||
"off": Z2MLight.TOGGLE,
|
||||
"hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class Philips929003017102LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
# "left_press": Light.TOGGLE,
|
||||
"left_press_release": Light.TOGGLE,
|
||||
"left_hold": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"left_hold_release": Light.RELEASE,
|
||||
# "right_press": Light.TOGGLE,
|
||||
"right_press_release": Light.TOGGLE,
|
||||
"right_hold": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"right_hold_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1000: None, # Initial press
|
||||
1001: Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
1002: Light.TOGGLE,
|
||||
1003: Light.RELEASE,
|
||||
2000: None, # Initial press
|
||||
2001: Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
2002: Light.TOGGLE,
|
||||
2003: Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
# "left_press": Light.TOGGLE,
|
||||
"left_press_release": Light.TOGGLE,
|
||||
"left_hold": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"left_hold_release": Light.RELEASE,
|
||||
# "right_press": Light.TOGGLE,
|
||||
"right_short_release": Light.TOGGLE,
|
||||
"right_hold": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"right_long_release": Light.RELEASE,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class Philips929003017102Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
# "left_press": Z2MLight.TOGGLE,
|
||||
"left_press_release": Z2MLight.TOGGLE,
|
||||
"left_hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"left_hold_release": Z2MLight.RELEASE,
|
||||
# "right_press": Z2MLight.TOGGLE,
|
||||
"right_press_release": Z2MLight.TOGGLE,
|
||||
"right_hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"right_hold_release": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class PhilipsRDM002LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_1_press_release": Light.OFF,
|
||||
"button_1_hold": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"button_1_hold_release": Light.RELEASE,
|
||||
"button_2_press_release": Light.ON,
|
||||
"button_2_hold": Light.HOLD_BRIGHTNESS_UP,
|
||||
"button_2_hold_release": Light.RELEASE,
|
||||
"button_3_press_release": Light.CLICK_COLOR_DOWN,
|
||||
"button_3_hold": Light.HOLD_COLOR_DOWN,
|
||||
"button_3_hold_release": Light.RELEASE,
|
||||
"button_4_press_release": Light.CLICK_COLOR_UP,
|
||||
"button_4_hold": Light.HOLD_COLOR_UP,
|
||||
"button_4_hold_release": Light.RELEASE,
|
||||
"dial_rotate_left_step": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"dial_rotate_left_slow": Light.ON_MIN_COLOR_TEMP,
|
||||
"dial_rotate_left_fast": Light.ON_MIN_BRIGHTNESS,
|
||||
"dial_rotate_right_step": Light.CLICK_BRIGHTNESS_UP,
|
||||
"dial_rotate_right_slow": Light.ON_FULL_COLOR_TEMP,
|
||||
"dial_rotate_right_fast": Light.ON_FULL_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class PhilipsRDM002Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_1_press_release": Z2MLight.OFF,
|
||||
"button_1_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"button_1_hold_release": Z2MLight.RELEASE,
|
||||
"button_2_press_release": Z2MLight.ON,
|
||||
"button_2_hold": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"button_2_hold_release": Z2MLight.RELEASE,
|
||||
"button_3_press_release": Z2MLight.CLICK_COLOR_TEMP_DOWN,
|
||||
"button_3_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN,
|
||||
"button_3_hold_release": Z2MLight.RELEASE,
|
||||
"button_4_press_release": Z2MLight.CLICK_COLOR_TEMP_UP,
|
||||
"button_4_hold": Z2MLight.HOLD_COLOR_TEMP_UP,
|
||||
"button_4_hold_release": Z2MLight.RELEASE,
|
||||
"dial_rotate_left_step": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"dial_rotate_left_slow": Z2MLight.ON_MIN_COLOR_TEMP,
|
||||
"dial_rotate_left_fast": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
"dial_rotate_right_step": Light.CLICK_BRIGHTNESS_UP,
|
||||
"dial_rotate_right_slow": Z2MLight.ON_FULL_COLOR_TEMP,
|
||||
"dial_rotate_right_fast": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
|
||||
|
||||
class Prolight5412748727388LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"off": Light.OFF,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
"brightness_move_to_level": Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL,
|
||||
"color_temperature_move": Light.COLORTEMP_FROM_CONTROLLER,
|
||||
"color_temperature_move_up": Light.CLICK_COLOR_TEMP_UP,
|
||||
"color_temperature_move_down": Light.CLICK_COLOR_TEMP_DOWN,
|
||||
"color_move": Light.XYCOLOR_FROM_CONTROLLER,
|
||||
}
|
||||
|
||||
|
||||
class Prolight5412748727388Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.ON,
|
||||
"off": Z2MLight.OFF,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
"brightness_move_to_level": Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL,
|
||||
"color_temperature_move": Z2MLight.COLORTEMP_FROM_CONTROLLER,
|
||||
"color_temperature_move_up": Z2MLight.CLICK_COLOR_TEMP_UP,
|
||||
"color_temperature_move_down": Z2MLight.CLICK_COLOR_TEMP_DOWN,
|
||||
"color_move": Z2MLight.XYCOLOR_FROM_CONTROLLER,
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
from cx_core.controller import action
|
||||
from cx_core.integration import EventData
|
||||
from cx_core.integration.zha import ZHAIntegration
|
||||
|
||||
|
||||
class ZB5121LightController(LightController):
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"off": Light.OFF,
|
||||
"step_with_on_off_0_32_0": Light.CLICK_BRIGHTNESS_UP, # Click brightness up
|
||||
"move_with_on_off_0_50": Light.HOLD_BRIGHTNESS_UP, # Hold brightness up
|
||||
"step_with_on_off_1_32_0": Light.CLICK_BRIGHTNESS_DOWN, # Click brightness down
|
||||
"move_with_on_off_1_50": Light.HOLD_BRIGHTNESS_DOWN, # Hold brightness down
|
||||
"recall_0_1": None, # Click clapperboard
|
||||
"stop": Light.RELEASE, # long release
|
||||
}
|
||||
|
||||
|
||||
class ZB5122LightController(LightController):
|
||||
@action
|
||||
async def colortemp_from_controller(self, extra: EventData) -> None:
|
||||
if isinstance(self.integration, ZHAIntegration):
|
||||
await self._on(color_temp=extra["args"][0])
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON, # Click light on
|
||||
"off": Light.OFF, # Click light off
|
||||
"hold_brightness_up": Light.HOLD_BRIGHTNESS_UP, # Hold light on
|
||||
"hold_brightness_down": Light.HOLD_BRIGHTNESS_DOWN, # Hold light off
|
||||
"stop": Light.RELEASE, # long release
|
||||
"move_to_color": Light.CLICK_XY_COLOR_UP, # click RGB
|
||||
"move_hue": Light.HOLD_XY_COLOR_UP, # hold RGB
|
||||
"stop_move_hue": Light.RELEASE, # release RGB
|
||||
"move_to_color_temp": Light.COLORTEMP_FROM_CONTROLLER, # click CW
|
||||
"move_color_temp": Light.HOLD_COLOR_TEMP_TOGGLE, # hold CW
|
||||
"stop_move_step": Light.RELEASE, # release CW
|
||||
"recall_0_1": None, # Click clapperboard
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
if command == "move_with_on_off":
|
||||
return (
|
||||
"hold_brightness_up" if data["args"][0] == 0 else "hold_brightness_down"
|
||||
)
|
||||
elif command == "move_hue":
|
||||
return "stop_move_hue" if tuple(data["args"]) == (0, 0) else "move_hue"
|
||||
return command
|
||||
|
||||
|
||||
class ZB3009LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.TOGGLE,
|
||||
"off": Light.TOGGLE,
|
||||
"brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Light.RELEASE,
|
||||
"color_temperature_move_down": Light.CLICK_COLOR_TEMP_DOWN,
|
||||
"color_temperature_move_up": Light.CLICK_COLOR_TEMP_UP,
|
||||
"color_temperature_move": Light.COLORTEMP_FROM_CONTROLLER,
|
||||
"color_move": Light.XYCOLOR_FROM_CONTROLLER,
|
||||
"hue_move": None, # Play/pause button
|
||||
"recall_1": None, # Scene 1
|
||||
"recall_3": None, # Scene 2
|
||||
"recall_2": None, # Scene 3
|
||||
}
|
||||
|
||||
|
||||
class ZB3009Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.TOGGLE,
|
||||
"off": Z2MLight.TOGGLE,
|
||||
"brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop": Z2MLight.RELEASE,
|
||||
"color_temperature_move_down": Z2MLight.CLICK_COLOR_TEMP_DOWN,
|
||||
"color_temperature_move_up": Z2MLight.CLICK_COLOR_TEMP_UP,
|
||||
"color_temperature_move": Z2MLight.COLORTEMP_FROM_CONTROLLER,
|
||||
"color_move": Z2MLight.XYCOLOR_FROM_CONTROLLER,
|
||||
"hue_move": None, # Play/pause button
|
||||
"recall_1": None, # Scene 1
|
||||
"recall_3": None, # Scene 2
|
||||
"recall_2": None, # Scene 3
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
|
||||
|
||||
class ROB2000070LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_1": Light.ON,
|
||||
"off_1": Light.OFF,
|
||||
"brightness_move_up_1": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_1": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_1": Light.RELEASE,
|
||||
"on_2": Light.ON,
|
||||
"off_2": Light.OFF,
|
||||
"brightness_move_up_2": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_2": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_2": Light.RELEASE,
|
||||
"on_3": Light.ON,
|
||||
"off_3": Light.OFF,
|
||||
"brightness_move_up_3": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_3": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_3": Light.RELEASE,
|
||||
"on_4": Light.ON,
|
||||
"off_4": Light.OFF,
|
||||
"brightness_move_up_4": Light.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_4": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_4": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class ROB2000070Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on_1": Z2MLight.ON,
|
||||
"off_1": Z2MLight.OFF,
|
||||
"brightness_move_up_1": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_1": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_1": Z2MLight.RELEASE,
|
||||
"on_2": Z2MLight.ON,
|
||||
"off_2": Z2MLight.OFF,
|
||||
"brightness_move_up_2": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_2": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_2": Z2MLight.RELEASE,
|
||||
"on_3": Z2MLight.ON,
|
||||
"off_3": Z2MLight.OFF,
|
||||
"brightness_move_up_3": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_3": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_3": Z2MLight.RELEASE,
|
||||
"on_4": Z2MLight.ON,
|
||||
"off_4": Z2MLight.OFF,
|
||||
"brightness_move_up_4": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"brightness_move_down_4": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"brightness_stop_4": Z2MLight.RELEASE,
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
from cx_const import DefaultActionsMapping, Light, Z2MLight
|
||||
from cx_core import LightController, Z2MLightController
|
||||
|
||||
|
||||
class E1EG7FLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"on_long": Light.CLICK_COLOR_UP,
|
||||
"on_double": Light.ON_FULL_COLOR_TEMP,
|
||||
"up": Light.CLICK_BRIGHTNESS_UP,
|
||||
"up_long": Light.ON_FULL_BRIGHTNESS,
|
||||
"down": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"down_long": Light.ON_MIN_BRIGHTNESS,
|
||||
"off": Light.OFF,
|
||||
"off_long": Light.CLICK_COLOR_DOWN,
|
||||
"off_double": Light.ON_MIN_COLOR_TEMP,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"on_long": Light.CLICK_COLOR_UP,
|
||||
"on_double": Light.ON_FULL_COLOR_TEMP,
|
||||
"step_0_1_0": Light.CLICK_BRIGHTNESS_UP,
|
||||
"step_0_2_0": Light.ON_FULL_BRIGHTNESS,
|
||||
"step_1_1_0": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"step_1_2_0": Light.ON_MIN_BRIGHTNESS,
|
||||
"off": Light.OFF,
|
||||
"off_long": Light.CLICK_COLOR_DOWN,
|
||||
"off_double": Light.ON_MIN_COLOR_TEMP,
|
||||
}
|
||||
|
||||
|
||||
class E1EG7FZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.ON,
|
||||
"on_long": Z2MLight.CLICK_COLOR_TEMP_UP,
|
||||
"on_double": Z2MLight.ON_FULL_COLOR_TEMP,
|
||||
"up": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"up_long": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"down": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"down_long": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
"off": Z2MLight.OFF,
|
||||
"off_long": Z2MLight.CLICK_COLOR_TEMP_DOWN,
|
||||
"off_double": Z2MLight.ON_MIN_COLOR_TEMP,
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
from cx_const import DefaultActionsMapping, Light
|
||||
from cx_core import LightController
|
||||
|
||||
|
||||
class ShellyI3LightController(LightController):
|
||||
def get_shellyforhass_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.CLICK_BRIGHTNESS_UP,
|
||||
"long": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class ShellyPlusI4LightController(LightController):
|
||||
def get_shelly_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_push_1": Light.ON,
|
||||
"long_push_1": Light.HOLD_COLOR_UP,
|
||||
"btn_up_1": Light.RELEASE,
|
||||
"double_push_1": Light.ON_FULL_COLOR_TEMP,
|
||||
"single_push_2": Light.OFF,
|
||||
"long_push_2": Light.HOLD_COLOR_DOWN,
|
||||
"btn_up_2": Light.RELEASE,
|
||||
"double_push_2": Light.ON_MIN_COLOR_TEMP,
|
||||
"single_push_3": Light.CLICK_BRIGHTNESS_UP,
|
||||
"long_push_3": Light.HOLD_BRIGHTNESS_UP,
|
||||
"btn_up_3": Light.RELEASE,
|
||||
"double_push_3": Light.ON_FULL_BRIGHTNESS,
|
||||
"single_push_4": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"long_push_4": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"btn_up_4": Light.RELEASE,
|
||||
"double_push_4": Light.ON_MIN_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class Shelly25LightController(LightController):
|
||||
def get_shelly_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_push_1": Light.ON,
|
||||
"long_push_1": Light.HOLD_BRIGHTNESS_UP,
|
||||
"btn_up_1": Light.RELEASE,
|
||||
"double_push_1": Light.ON_FULL_BRIGHTNESS,
|
||||
"single_push_2": Light.OFF,
|
||||
"long_push_2": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"btn_up_2": Light.RELEASE,
|
||||
"double_push_2": Light.ON_MIN_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class ShellyDimmer2LightController(LightController):
|
||||
def get_shelly_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_1": Light.TOGGLE,
|
||||
"single_2": None,
|
||||
"long_1": Light.TOGGLE,
|
||||
"long_2": None,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
from cx_const import DefaultActionsMapping, Light
|
||||
from cx_core import LightController
|
||||
|
||||
|
||||
class SK5700002228949LightController(LightController):
|
||||
# This mapping works for: 5700002228949, 5700002228963, 5745000433087
|
||||
# The buttons are distributted like:
|
||||
# 1 3
|
||||
# 2 4
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.ON,
|
||||
1001: Light.HOLD_BRIGHTNESS_UP,
|
||||
1003: Light.RELEASE,
|
||||
3002: Light.OFF,
|
||||
3001: Light.HOLD_BRIGHTNESS_DOWN,
|
||||
3003: Light.RELEASE,
|
||||
2002: Light.CLICK_COLOR_UP,
|
||||
2001: Light.HOLD_COLOR_UP,
|
||||
2003: Light.RELEASE,
|
||||
4002: Light.CLICK_COLOR_DOWN,
|
||||
4001: Light.HOLD_COLOR_DOWN,
|
||||
4003: Light.RELEASE,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
from cx_const import DefaultActionsMapping, Light, MediaPlayer
|
||||
from cx_core import LightController, MediaPlayerController
|
||||
|
||||
|
||||
class SmartThingsButtonLightController(LightController):
|
||||
"""
|
||||
This controller sends click, double click, and hold commands.
|
||||
No release command is sent.
|
||||
"""
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
"hold": Light.SET_HALF_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: Light.TOGGLE,
|
||||
1004: Light.ON_FULL_BRIGHTNESS,
|
||||
1001: Light.SET_HALF_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_single_1_0_0_0": Light.TOGGLE,
|
||||
"button_double_2_0_0_0": Light.ON_FULL_BRIGHTNESS,
|
||||
"button_hold_3_0_0_0": Light.SET_HALF_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class SmartThingsButtonMediaPlayerController(MediaPlayerController):
|
||||
"""
|
||||
This controller sends click, double click, and hold commands.
|
||||
No release command is sent.
|
||||
"""
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single_click": MediaPlayer.PLAY_PAUSE,
|
||||
"double_click": MediaPlayer.NEXT_TRACK,
|
||||
"hold": MediaPlayer.PREVIOUS_TRACK,
|
||||
}
|
||||
|
||||
def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
1002: MediaPlayer.PLAY_PAUSE,
|
||||
1004: MediaPlayer.NEXT_TRACK,
|
||||
1001: MediaPlayer.PREVIOUS_TRACK,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_single_1_0_0_0": MediaPlayer.PLAY_PAUSE,
|
||||
"button_double_2_0_0_0": MediaPlayer.NEXT_TRACK,
|
||||
"button_hold_3_0_0_0": MediaPlayer.PREVIOUS_TRACK,
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
from cx_const import DefaultActionsMapping, Light
|
||||
from cx_core import LightController
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class SNZB01LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"single": Light.TOGGLE, # single click
|
||||
"double": Light.ON_FULL_BRIGHTNESS, # double click
|
||||
"long": Light.ON_MIN_BRIGHTNESS, # hold
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"toggle": Light.TOGGLE, # single click
|
||||
"on": Light.ON_FULL_BRIGHTNESS, # double click
|
||||
"off": Light.ON_MIN_BRIGHTNESS, # hold
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
@@ -0,0 +1,103 @@
|
||||
from cx_const import Cover, DefaultActionsMapping, Light, Switch, Z2MLight
|
||||
from cx_core import (
|
||||
CoverController,
|
||||
LightController,
|
||||
SwitchController,
|
||||
Z2MLightController,
|
||||
)
|
||||
|
||||
|
||||
class TasmotaButtonZ2MLightController(Z2MLightController):
|
||||
def get_tasmota_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"TOGGLE": Z2MLight.TOGGLE,
|
||||
"SINGLE": Z2MLight.TOGGLE,
|
||||
"DOUBLE": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
"TRIPLE": Z2MLight.SET_HALF_BRIGHTNESS,
|
||||
"QUAD": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"PENTA": None,
|
||||
"HOLD": Z2MLight.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"CLEAR": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class TasmotaSwitchZ2MLightController(Z2MLightController):
|
||||
def get_tasmota_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"TOGGLE": Z2MLight.TOGGLE,
|
||||
"ON": Z2MLight.ON,
|
||||
"OFF": Z2MLight.OFF,
|
||||
"HOLD": Z2MLight.SET_HALF_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class TasmotaButtonLightController(LightController):
|
||||
def get_tasmota_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"TOGGLE": Light.TOGGLE,
|
||||
"SINGLE": Light.TOGGLE,
|
||||
"DOUBLE": Light.ON_MIN_BRIGHTNESS,
|
||||
"TRIPLE": Light.SET_HALF_BRIGHTNESS,
|
||||
"QUAD": Light.ON_FULL_BRIGHTNESS,
|
||||
"PENTA": None,
|
||||
"HOLD": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"CLEAR": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class TasmotaSwitchLightController(LightController):
|
||||
def get_tasmota_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"TOGGLE": Light.TOGGLE,
|
||||
"ON": Light.ON,
|
||||
"OFF": Light.OFF,
|
||||
"HOLD": Light.ON_MIN_MAX_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class TasmotaButtonSwitchController(SwitchController):
|
||||
def get_tasmota_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"TOGGLE": Light.TOGGLE,
|
||||
"SINGLE": Switch.TOGGLE,
|
||||
"DOUBLE": Switch.ON,
|
||||
"TRIPLE": Switch.OFF,
|
||||
"QUAD": None,
|
||||
"PENTA": None,
|
||||
"HOLD": None,
|
||||
"CLEAR": None,
|
||||
}
|
||||
|
||||
|
||||
class TasmotaSwitchSwitchController(SwitchController):
|
||||
def get_tasmota_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"TOGGLE": Switch.TOGGLE,
|
||||
"ON": Switch.ON,
|
||||
"OFF": Switch.OFF,
|
||||
"HOLD": None,
|
||||
}
|
||||
|
||||
|
||||
class TasmotaButtonCoverController(CoverController):
|
||||
def get_tasmota_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"TOGGLE": Cover.TOGGLE_OPEN,
|
||||
"SINGLE": Cover.TOGGLE_OPEN,
|
||||
"DOUBLE": Cover.CLOSE,
|
||||
"TRIPLE": None,
|
||||
"QUAD": None,
|
||||
"PENTA": None,
|
||||
"HOLD": Cover.CLOSE,
|
||||
"CLEAR": None,
|
||||
}
|
||||
|
||||
|
||||
class TasmotaSwitchCoverController(CoverController):
|
||||
def get_tasmota_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"TOGGLE": Cover.TOGGLE_OPEN,
|
||||
"ON": Cover.OPEN,
|
||||
"OFF": Cover.CLOSE,
|
||||
"HOLD": Cover.TOGGLE_CLOSE,
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
from cx_const import DefaultActionsMapping, Light, MediaPlayer
|
||||
from cx_core import LightController, MediaPlayerController
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class TerncyPP01LightController(LightController):
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_single": Light.TOGGLE,
|
||||
"button_double": Light.ON_FULL_BRIGHTNESS,
|
||||
"button_triple": Light.ON_MIN_BRIGHTNESS,
|
||||
"button_quadruple": Light.SET_HALF_BRIGHTNESS,
|
||||
"button_quintuple": Light.SET_HALF_COLOR_TEMP,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class TerncySD01LightController(LightController):
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_single": Light.TOGGLE,
|
||||
"button_double": Light.ON_FULL_BRIGHTNESS,
|
||||
"button_triple": Light.ON_MIN_BRIGHTNESS,
|
||||
"button_quadruple": Light.SET_HALF_BRIGHTNESS,
|
||||
"button_quintuple": Light.SET_HALF_COLOR_TEMP,
|
||||
"rotate_left": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"rotate_right": Light.CLICK_BRIGHTNESS_UP,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
|
||||
|
||||
class TerncySD01MediaPlayerController(MediaPlayerController):
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"button_single": MediaPlayer.PLAY_PAUSE,
|
||||
"button_double": MediaPlayer.MUTE,
|
||||
"button_triple": MediaPlayer.NEXT_TRACK,
|
||||
"button_quadruple": MediaPlayer.PREVIOUS_TRACK,
|
||||
"button_quintuple": MediaPlayer.NEXT_SOURCE,
|
||||
"rotate_left": MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
"rotate_right": MediaPlayer.CLICK_VOLUME_UP,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
return command
|
||||
@@ -0,0 +1,39 @@
|
||||
from cx_const import DefaultActionsMapping, Light, MediaPlayer, Z2MLight
|
||||
from cx_core import LightController, MediaPlayerController, Z2MLightController
|
||||
|
||||
|
||||
class ZYCT202LightController(LightController):
|
||||
"""
|
||||
This controller does not send hold action for on/off
|
||||
"""
|
||||
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.ON,
|
||||
"up-press": Light.HOLD_BRIGHTNESS_UP,
|
||||
"down-press": Light.HOLD_BRIGHTNESS_DOWN,
|
||||
"off": Light.OFF,
|
||||
"stop": Light.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class ZYCT202Z2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Z2MLight.ON,
|
||||
"up-press": Z2MLight.HOLD_BRIGHTNESS_UP,
|
||||
"down-press": Z2MLight.HOLD_BRIGHTNESS_DOWN,
|
||||
"off": Z2MLight.OFF,
|
||||
"stop": Z2MLight.RELEASE,
|
||||
}
|
||||
|
||||
|
||||
class ZYCT202MediaPlayerController(MediaPlayerController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": MediaPlayer.PLAY_PAUSE,
|
||||
"up-press": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"down-press": MediaPlayer.HOLD_VOLUME_DOWN,
|
||||
"off": MediaPlayer.PLAY_PAUSE,
|
||||
"stop": MediaPlayer.RELEASE,
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
from cx_const import Cover, DefaultActionsMapping, Light, MediaPlayer, Z2MLight
|
||||
from cx_core import (
|
||||
CoverController,
|
||||
LightController,
|
||||
MediaPlayerController,
|
||||
Z2MLightController,
|
||||
)
|
||||
from cx_core.integration import EventData
|
||||
|
||||
|
||||
class TS0044LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_single": Light.TOGGLE,
|
||||
"1_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"1_hold": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"2_single": Light.TOGGLE,
|
||||
"2_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"2_hold": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"3_single": Light.TOGGLE,
|
||||
"3_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"3_hold": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"4_single": Light.TOGGLE,
|
||||
"4_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"4_hold": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
|
||||
class TS0044FLightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"on": Light.TOGGLE,
|
||||
"off": Light.TOGGLE,
|
||||
"brightness_step_up": Light.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_step_down": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
|
||||
class TS0043LightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_single": Light.TOGGLE,
|
||||
"1_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"1_hold": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"2_single": Light.TOGGLE,
|
||||
"2_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"2_hold": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"3_single": Light.TOGGLE,
|
||||
"3_double": Light.CLICK_BRIGHTNESS_UP,
|
||||
"3_hold": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_remote_button_short_press": Light.TOGGLE,
|
||||
"1_remote_button_double_press": Light.CLICK_BRIGHTNESS_UP,
|
||||
"1_remote_button_long_press": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"2_remote_button_short_press": Light.TOGGLE,
|
||||
"2_remote_button_double_press": Light.CLICK_BRIGHTNESS_UP,
|
||||
"2_remote_button_long_press": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"3_remote_button_short_press": Light.TOGGLE,
|
||||
"3_remote_button_double_press": Light.CLICK_BRIGHTNESS_UP,
|
||||
"3_remote_button_long_press": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
args: str = ""
|
||||
# Command is {endpoint_id}_{command}
|
||||
if len(data["args"]) > 0:
|
||||
if isinstance(data["args"], list):
|
||||
args = "_" + "_".join([str(d) for d in data["args"]])
|
||||
else:
|
||||
args = args + f"{data['args']}"
|
||||
command: str = f"{data['endpoint_id']}_{data['command']}{args}"
|
||||
return command
|
||||
|
||||
|
||||
class TS0043CoverController(CoverController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_single": Cover.OPEN,
|
||||
"2_single": Cover.STOP,
|
||||
"3_single": Cover.CLOSE,
|
||||
}
|
||||
|
||||
|
||||
class TuYaERS10TZBVKAALightController(LightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
# Command mode
|
||||
"brightness_step_up": Light.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_step_down": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"toggle": Light.TOGGLE,
|
||||
"hue_move": Light.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"hue_stop": Light.RELEASE,
|
||||
"color_temperature_step_up": Light.CLICK_COLOR_UP,
|
||||
"color_temperature_step_down": Light.CLICK_COLOR_DOWN,
|
||||
# Event mode
|
||||
"rotate_left": Light.CLICK_BRIGHTNESS_DOWN,
|
||||
"rotate_right": Light.CLICK_BRIGHTNESS_UP,
|
||||
"single": Light.TOGGLE,
|
||||
"double": Light.ON_FULL_BRIGHTNESS,
|
||||
"hold": Light.ON_MIN_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"toggle": Light.TOGGLE,
|
||||
"step_brightness": Light.BRIGHTNESS_FROM_CONTROLLER_STEP,
|
||||
"step_color_temp": Light.COLORTEMP_FROM_CONTROLLER_STEP,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
command: str = data["command"]
|
||||
if command == "step":
|
||||
return "step_brightness"
|
||||
return command
|
||||
|
||||
|
||||
class TuYaERS10TZBVKAAZ2MLightController(Z2MLightController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
# Command mode
|
||||
"brightness_step_up": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"brightness_step_down": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"toggle": Z2MLight.TOGGLE,
|
||||
"hue_move": Z2MLight.HOLD_BRIGHTNESS_TOGGLE,
|
||||
"hue_stop": Z2MLight.RELEASE,
|
||||
"color_temperature_step_up": Z2MLight.CLICK_COLOR_TEMP_UP,
|
||||
"color_temperature_step_down": Z2MLight.CLICK_COLOR_TEMP_DOWN,
|
||||
# Event mode
|
||||
"rotate_left": Z2MLight.CLICK_BRIGHTNESS_DOWN,
|
||||
"rotate_right": Z2MLight.CLICK_BRIGHTNESS_UP,
|
||||
"single": Z2MLight.TOGGLE,
|
||||
"double": Z2MLight.ON_FULL_BRIGHTNESS,
|
||||
"hold": Z2MLight.ON_MIN_BRIGHTNESS,
|
||||
}
|
||||
|
||||
|
||||
class TuYaERS10TZBVKAAMediaPlayerController(MediaPlayerController):
|
||||
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
# Command mode
|
||||
"brightness_step_up": MediaPlayer.CLICK_VOLUME_UP,
|
||||
"brightness_step_down": MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
"toggle": MediaPlayer.PLAY_PAUSE,
|
||||
"hue_move": MediaPlayer.HOLD_VOLUME_UP,
|
||||
"hue_stop": Z2MLight.RELEASE,
|
||||
"color_temperature_step_up": MediaPlayer.NEXT_TRACK,
|
||||
"color_temperature_step_down": MediaPlayer.PREVIOUS_TRACK,
|
||||
# Event mode
|
||||
"rotate_left": MediaPlayer.CLICK_VOLUME_DOWN,
|
||||
"rotate_right": MediaPlayer.CLICK_VOLUME_UP,
|
||||
"single": MediaPlayer.PLAY_PAUSE,
|
||||
"double": MediaPlayer.NEXT_TRACK,
|
||||
"hold": MediaPlayer.PREVIOUS_TRACK,
|
||||
}
|
||||
|
||||
|
||||
class TS0042LightController(LightController):
|
||||
def get_zha_actions_mapping(self) -> DefaultActionsMapping:
|
||||
return {
|
||||
"1_remote_button_short_press": Light.ON,
|
||||
"1_remote_button_double_press": Light.CLICK_BRIGHTNESS_UP,
|
||||
"1_remote_button_long_press": Light.ON_FULL_BRIGHTNESS,
|
||||
"2_remote_button_short_press": Light.OFF,
|
||||
"2_remote_button_double_press": Light.ON_MIN_BRIGHTNESS,
|
||||
}
|
||||
|
||||
def get_zha_action(self, data: EventData) -> str:
|
||||
args: str = ""
|
||||
# Command is {endpoint_id}_{command}
|
||||
if len(data["args"]) > 0:
|
||||
if isinstance(data["args"], list):
|
||||
args = "_" + "_".join([str(d) for d in data["args"]])
|
||||
else:
|
||||
args = args + f"{data['args']}"
|
||||
command: str = f"{data['endpoint_id']}_{data['command']}{args}"
|
||||
return command
|
||||
@@ -0,0 +1,33 @@
|
||||
import importlib
|
||||
import os
|
||||
import pkgutil
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _import_modules(file_dir: str, package: str) -> None:
|
||||
pkg_dir = os.path.dirname(file_dir)
|
||||
for _, name, ispkg in pkgutil.iter_modules([pkg_dir]):
|
||||
if ispkg:
|
||||
_import_modules(pkg_dir + "/" + name + "/__init__.py", package + "." + name)
|
||||
else:
|
||||
importlib.import_module("." + name, package)
|
||||
|
||||
|
||||
def _all_subclasses(cls: type[Any]) -> list[type[Any]]:
|
||||
return list(
|
||||
set(type.__subclasses__(cls)).union(
|
||||
[s for c in type.__subclasses__(cls) for s in _all_subclasses(c)]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_classes(file_: str, package_: str, class_: type[Any]) -> list[type[Any]]:
|
||||
_import_modules(file_, package_)
|
||||
subclasses = _all_subclasses(class_)
|
||||
subclasses = [cls_ for cls_ in subclasses if f"{package_}." in cls_.__module__]
|
||||
return subclasses
|
||||
|
||||
|
||||
def get_instances(file_: str, package_: str, class_: type[Any]) -> list[Any]:
|
||||
classes = get_classes(file_, package_, class_)
|
||||
return [cls_() for cls_ in classes]
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "v5.2.3"
|
||||
@@ -0,0 +1,386 @@
|
||||
- id: '1726037322181'
|
||||
alias: WC Sensor Light
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/sensor-light.yaml
|
||||
input:
|
||||
motion_trigger:
|
||||
- binary_sensor.wc_bewegungsmelder_occupancy
|
||||
light_switch:
|
||||
entity_id: light.wc_decke
|
||||
include_sun: sun_disabled
|
||||
night_lights:
|
||||
entity_id: light.wc_decke
|
||||
night_light_brightness: 30
|
||||
- id: '1726044464634'
|
||||
alias: Badezimmer Sensor Light
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/sensor-light.yaml
|
||||
input:
|
||||
motion_trigger:
|
||||
- binary_sensor.badezimmer_bewegungsmelder_occupancy
|
||||
light_switch:
|
||||
entity_id: light.badezimmer_decke
|
||||
include_sun: sun_disabled
|
||||
night_lights:
|
||||
entity_id: light.badezimmer_decke
|
||||
night_light_brightness: 30
|
||||
time_delay: 15
|
||||
- id: '1726159872511'
|
||||
alias: Wassermeldung
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/state-notifications-and-actions.yaml
|
||||
input:
|
||||
start_trigger_state_entity:
|
||||
- binary_sensor.wc_wassermelder_water_leak
|
||||
- binary_sensor.technikraum_wassermelder_water_leak
|
||||
- binary_sensor.kuche_wassermelder_water_leak
|
||||
start_notify_device:
|
||||
- 5fb7463ffcf5af63b5b871d88de9384d
|
||||
- b66802e4b09680de467594c871f22d67
|
||||
start_title: WASSERALARM
|
||||
start_message: '{{ states[trigger.entity_id].attributes.friendly_name }} hat
|
||||
Feuchtigkeit festgestellt.'
|
||||
start_notify_data:
|
||||
- high_priority
|
||||
- sticky
|
||||
start_trigger_state: 'on'
|
||||
include_start_notify: enable_start_notify_options
|
||||
- id: '1726164370652'
|
||||
alias: Büro Anwesenheit
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/sensor-light.yaml
|
||||
input:
|
||||
motion_trigger:
|
||||
- binary_sensor.buro_anwesenheitssensor_raum
|
||||
light_switch:
|
||||
entity_id:
|
||||
- light.buro_decke
|
||||
time_delay: 0.5
|
||||
include_sun: sun_enabled
|
||||
include_light_colour_control: disable_colour_control
|
||||
ambient_light_sensor: sensor.buro_anwesenheitssensor_light_sensor_light_level
|
||||
ambient_light_options: ambient_light_option_enabled
|
||||
include_ambient: ambient_enabled
|
||||
ambient_light_high_value: 60
|
||||
- id: '1726175048536'
|
||||
alias: Low Battery
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/low-battery-notifications-and-actions.yaml
|
||||
input:
|
||||
include_time: time_enabled
|
||||
include_easy_notify: disabled_easy_notify
|
||||
include_persistent_notification: disabled_persistent_notification
|
||||
notify_device: []
|
||||
time: '16:00:00'
|
||||
exclude_sensors:
|
||||
entity_id:
|
||||
- sensor.powerocean_bpack1_bpsoc
|
||||
- sensor.powerocean_bpack2_bpsoc
|
||||
- sensor.powerocean_bpsoc_2
|
||||
- sensor.pixel_8_pro_car_battery
|
||||
- sensor.karolina_tracker_batterie
|
||||
include_custom_actions: enable_custom_actions
|
||||
custom_actions:
|
||||
- action: notify.send_message
|
||||
data:
|
||||
title: Low battery warning
|
||||
message: '{{sensors}}'
|
||||
entity_id: notify.home_assistant
|
||||
- id: '1726304621477'
|
||||
alias: Speis Sensor Light
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/sensor-light.yaml
|
||||
input:
|
||||
motion_trigger:
|
||||
- binary_sensor.speis_bewegungsmelder_occupancy
|
||||
light_switch:
|
||||
entity_id: light.speis_decke
|
||||
time_delay: 0.5
|
||||
- id: '1727623123034'
|
||||
alias: Badheizung
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/timer.yaml
|
||||
input:
|
||||
entity_input:
|
||||
entity_id: switch.badezimmer_handtuchheizung
|
||||
trigger_one_time: 05:15:00
|
||||
trigger_one_duration:
|
||||
hours: 0
|
||||
minutes: 15
|
||||
seconds: 0
|
||||
include_weekdays: weekday_enabled
|
||||
weekday_options:
|
||||
- mon
|
||||
- tue
|
||||
- wed
|
||||
- thu
|
||||
- fri
|
||||
timer_helper: timer.timer_badezimmer_heizung
|
||||
- id: '1727976866531'
|
||||
alias: Reminder Abfallkalender
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/calendar-notifications-and-actions.yaml
|
||||
input:
|
||||
calendar: calendar.abfallkalender
|
||||
include_calendar_trigger: disable_key_word
|
||||
start_calendar_offset:
|
||||
hours: -12
|
||||
minutes: 0
|
||||
seconds: 0
|
||||
days: 0
|
||||
start_notify_device:
|
||||
- 5fb7463ffcf5af63b5b871d88de9384d
|
||||
- b66802e4b09680de467594c871f22d67
|
||||
include_start_notify: enable_start_notify_options
|
||||
- id: '1729356542767'
|
||||
alias: Nachtmodus Button
|
||||
description: ''
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id:
|
||||
- input_button.nachtmodus
|
||||
conditions: []
|
||||
actions:
|
||||
- action: light.turn_off
|
||||
metadata: {}
|
||||
data: {}
|
||||
target:
|
||||
area_id:
|
||||
- badezimmer
|
||||
- buro
|
||||
- esszimmer
|
||||
- gang
|
||||
- gaderobe
|
||||
- speis
|
||||
- technikraum
|
||||
- wc
|
||||
- wohnzimmer
|
||||
- kuche
|
||||
- terrasse
|
||||
- eingang
|
||||
- gastezimmer
|
||||
- grillplatz
|
||||
- ruheplatz
|
||||
mode: single
|
||||
- id: '1729414838563'
|
||||
alias: Geschirrspüler Nachtbetrieb
|
||||
description: ''
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id:
|
||||
- binary_sensor.cheapest_window_dishwasher
|
||||
to: 'on'
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id: input_boolean.geschirrspuler_uber_nacht
|
||||
state: 'on'
|
||||
actions:
|
||||
- type: turn_on
|
||||
device_id: 04ff0a4c8bbdb791bc2f414ef87557e0
|
||||
entity_id: 37e7a92fe454c1a3a9eb60ff311e6c85
|
||||
domain: switch
|
||||
- device_id: 04ff0a4c8bbdb791bc2f414ef87557e0
|
||||
domain: select
|
||||
entity_id: f15c038737129ff56b3fd0dbdf3f23db
|
||||
type: select_option
|
||||
option: Dishcare.Dishwasher.Program.Eco50
|
||||
- device_id: 04ff0a4c8bbdb791bc2f414ef87557e0
|
||||
domain: button
|
||||
entity_id: f925a6bf1e774246be1c06f6d9fd7fe6
|
||||
type: press
|
||||
enabled: true
|
||||
- action: input_boolean.turn_off
|
||||
metadata: {}
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.geschirrspuler_uber_nacht
|
||||
mode: single
|
||||
- id: '1731252691843'
|
||||
alias: Test sugbna
|
||||
description: ''
|
||||
triggers: []
|
||||
conditions: []
|
||||
actions:
|
||||
- action: notify.send_message
|
||||
data:
|
||||
message: 'Reminder: Have you considered frogs?'
|
||||
entity_id: notify.home_assistant
|
||||
mode: single
|
||||
- id: '1732639576586'
|
||||
alias: Rauchmeldeung
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/state-notifications-and-actions.yaml
|
||||
input:
|
||||
start_trigger_state_entity:
|
||||
- binary_sensor.buro_rauchmelder_smoke
|
||||
- binary_sensor.wohnzimmer_rauchmelder_smoke
|
||||
- binary_sensor.gastezimmer_rauchmelder_smoke
|
||||
- binary_sensor.technikraum_rauchmelder_smoke
|
||||
- binary_sensor.kinderzimmer_rauchmelder_smoke
|
||||
- binary_sensor.schlafzimmer_rauchmelder_smoke
|
||||
start_notify_device:
|
||||
- 5fb7463ffcf5af63b5b871d88de9384d
|
||||
- b66802e4b09680de467594c871f22d67
|
||||
start_title: RAUCHALARM
|
||||
start_message: '{{ states[trigger.entity_id].attributes.friendly_name }} hat
|
||||
Rauch festgestellt.'
|
||||
start_notify_data:
|
||||
- high_priority
|
||||
- sticky
|
||||
start_trigger_state: 'on'
|
||||
include_start_notify: enable_start_notify_options
|
||||
- id: '1734279266836'
|
||||
alias: Technikraum Bewegungsmelder
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/sensor-light.yaml
|
||||
input:
|
||||
motion_trigger:
|
||||
- binary_sensor.technikraum_bewegungsmelder_occupancy
|
||||
light_switch:
|
||||
entity_id: light.technikraum_decke
|
||||
time_delay: 0.5
|
||||
- id: '1746210944619'
|
||||
alias: Wetterwarnung DWD
|
||||
description: ''
|
||||
triggers:
|
||||
- entity_id: sensor.mitgliedsgemeinde_in_verwaltungsgemeinschaft_welden_aktuelle_warnstufe
|
||||
above: 0
|
||||
trigger: numeric_state
|
||||
conditions: []
|
||||
actions:
|
||||
- data:
|
||||
message: '{{ state_attr(''mitgliedsgemeinde_in_verwaltungsgemeinschaft_welden_aktuelle_warnstufe'',
|
||||
''warning_1_description'') }} Von: {{ as_timestamp(state_attr(''mitgliedsgemeinde_in_verwaltungsgemeinschaft_welden_aktuelle_warnstufe'',
|
||||
''warning_1_start'')) | timestamp_custom(''%d.%m.%Y, %H:%M'')}} Uhr Bis: {{
|
||||
as_timestamp(state_attr(''mitgliedsgemeinde_in_verwaltungsgemeinschaft_welden_aktuelle_warnstufe'',
|
||||
''warning_1_end'')) | timestamp_custom(''%d.%m.%Y, %H:%M'')}} Uhr'
|
||||
title: '{{ state_attr(''sensor.mitgliedsgemeinde_in_verwaltungsgemeinschaft_welden_aktuelle_warnstufe'',
|
||||
''warning_1_name'') }} ⛈️'
|
||||
action: notify.send_message
|
||||
entity_id: notify.home_assistant
|
||||
mode: single
|
||||
- id: '1746266479215'
|
||||
alias: Kinderzimmer Sensor Light
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/sensor-light.yaml
|
||||
input:
|
||||
motion_trigger:
|
||||
- binary_sensor.kinderzimmer_bewegungsmelder_occupancy
|
||||
light_switch:
|
||||
entity_id: light.garderobe_decke
|
||||
include_light_control:
|
||||
- use_brightness
|
||||
light_brightness: 100
|
||||
include_ambient: ambient_disabled
|
||||
ambient_light_sensor: sensor.kinderzimmer_rauchmelder_illuminance
|
||||
ambient_light_options: ambient_light_option_enabled
|
||||
time_delay: 0.5
|
||||
ambient_light_value: 20
|
||||
include_sun: sun_disabled
|
||||
after_time: '18:00:00'
|
||||
before_time: 08:00:00
|
||||
include_time: time_disabled
|
||||
include_light_colour_control: disable_colour_control
|
||||
light_rgb_colour:
|
||||
- 117
|
||||
- 53
|
||||
- 21
|
||||
light_transition_on: 0
|
||||
light_transition_off: 0
|
||||
boolean_scenes_scripts: input_boolean.garderobe_helper
|
||||
light_colour_temperature: 2000
|
||||
- id: '1776921027487'
|
||||
alias: Tracker Reminder
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: Blackshome/low-battery-notifications-and-actions.yaml
|
||||
input:
|
||||
battery_level: 29
|
||||
custom_group:
|
||||
entity_id:
|
||||
- sensor.karolina_tracker_batterie_history
|
||||
include_easy_notify: enable_easy_notify
|
||||
notify_device:
|
||||
- b66802e4b09680de467594c871f22d67
|
||||
- 5fb7463ffcf5af63b5b871d88de9384d
|
||||
notify_title: Tracker laden!
|
||||
sensor_selection: enable_battery_sensors_custom_group
|
||||
notify_data: []
|
||||
time: '17:00:00'
|
||||
custom_group_battery_level: 29
|
||||
include_persistent_notification: enable_persistent_notification
|
||||
- id: '1777224364774'
|
||||
alias: Befeuchter ein – Luftfeuchtigkeit unter 50%
|
||||
description: ''
|
||||
triggers:
|
||||
- trigger: numeric_state
|
||||
entity_id: sensor.wohnzimmer_rauchmelder_humidity
|
||||
below: 50
|
||||
conditions: []
|
||||
actions:
|
||||
- action: humidifier.set_mode
|
||||
target:
|
||||
entity_id:
|
||||
- humidifier.evaporative_humidifier_05de
|
||||
data:
|
||||
mode: low
|
||||
mode: single
|
||||
- id: '1777224379805'
|
||||
alias: Befeuchter aus – Luftfeuchtigkeit über 60%
|
||||
description: ''
|
||||
triggers:
|
||||
- trigger: numeric_state
|
||||
entity_id: sensor.wohnzimmer_rauchmelder_humidity
|
||||
above: 60
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id: humidifier.evaporative_humidifier_05de
|
||||
state: 'on'
|
||||
actions:
|
||||
- action: humidifier.turn_off
|
||||
target:
|
||||
entity_id: humidifier.evaporative_humidifier_05de
|
||||
mode: single
|
||||
- id: '1777224493981'
|
||||
alias: Befeuchter – Wasserstand niedrig
|
||||
description: ''
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: sensor.evaporative_humidifier_05de_wasserstand
|
||||
to: niedrig
|
||||
conditions: []
|
||||
actions:
|
||||
- action: notify.notify
|
||||
data:
|
||||
title: "\U0001F4A7 Befeuchter Wasser leer"
|
||||
message: Der Wasserstand des Befeuchters ist niedrig – bitte nachfüllen.
|
||||
mode: single
|
||||
- id: '1777224986079'
|
||||
alias: BYD Dolphin Position aktualisieren
|
||||
description: ''
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id:
|
||||
- sensor.byd_dolphin_60_5_kwh_abrp_latitude
|
||||
- sensor.byd_dolphin_60_5_kwh_abrp_longitude
|
||||
conditions: []
|
||||
actions:
|
||||
- action: device_tracker.see
|
||||
data:
|
||||
dev_id: byd_dolphin
|
||||
gps:
|
||||
- '{{ states(''sensor.byd_dolphin_60_5_kwh_abrp_latitude'') | float }}'
|
||||
- '{{ states(''sensor.byd_dolphin_60_5_kwh_abrp_longitude'') | float }}'
|
||||
gps_accuracy: 10
|
||||
mode: single
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,601 @@
|
||||
blueprint:
|
||||
name: Notifications & Announcements
|
||||
description: "# \U0001F4E2 Notifications & Announcements\n**Version: 1.2**\n\nState
|
||||
your trigger and spread the word \U0001F4AC\U0001F509\n\n**If you like my blueprints,
|
||||
and would like to show your support or just say thank you?** [Click Here](https://www.paypal.com/donate/?hosted_button_id=WAZS3QSDTPGA8)
|
||||
\U0001F642\n<details> <summary><b>The Automation Process:</b> - Click here to
|
||||
expand</summary>\n\n\n - **Trigger Options:**\n\n - Specify the state change
|
||||
of one or more entities that will initiate the automation from a dropdown list.
|
||||
You can also specify your own custom state if it is not available from the dropdown
|
||||
list.\n - State Change Options: The available state changes include:\n -
|
||||
**General States:** Button or any state, ON, OFF, Unavailable or Unknown\n -
|
||||
**Location-Based States:** Home or Not Home\n - **Numeric States:** Numeric
|
||||
state above, Numeric state below or Numeric state above & below\n\n - **Notification
|
||||
Options:**\n\n - Opt to send messages to one or multiple devices. Compatible
|
||||
with Apple iOS and Android devices.\n - Opt to display your notifications directly
|
||||
within the Home Assistant user interface (UI).\n - Opt to send Text-to-Speech
|
||||
Announcement to your selected media players.\n\n - **Time-Based Automation:**\n\n
|
||||
\ - Specify precise start and end times along with weekday selections to define
|
||||
when the automation can run.\n\n - **Custom Conditions:**\n\n - Enter any
|
||||
custom conditions to further customize the automation process.\n\n</details>\n\nNeed
|
||||
help? See our FAQ: [Click Here](https://community.home-assistant.io/t/notifications-announcements/728100/2?u=blacky)\nLet
|
||||
us know what you think of this blueprint and for community support including updates:
|
||||
[Click Here](https://community.home-assistant.io/t/notifications-announcements/728100?u=blacky)\n\nRequired
|
||||
= *\n"
|
||||
domain: automation
|
||||
input:
|
||||
trigger_settings:
|
||||
name: Triggers *
|
||||
icon: mdi:cog-outline
|
||||
collapsed: true
|
||||
input:
|
||||
trigger_state:
|
||||
name: Trigger - State *
|
||||
description: 'Please select what state you would like for your trigger.
|
||||
You can choose from ten options available in the dropdown menu. If your
|
||||
option is not in the dropdown menu then just type the state you would
|
||||
like to use in and click the save button.
|
||||
|
||||
|
||||
1 - Button Or Any State
|
||||
|
||||
2 - ON
|
||||
|
||||
3 - OFF
|
||||
|
||||
4 - Unavailable
|
||||
|
||||
5 - Unknown
|
||||
|
||||
6 - Home
|
||||
|
||||
7 - Not Home
|
||||
|
||||
8 - Numeric State - Above
|
||||
|
||||
9 - Numeric State - Below
|
||||
|
||||
10 - Numeric State - Above & Below
|
||||
|
||||
|
||||
A numbering system has been implemented to facilitate navigation within
|
||||
the dropdown selections. Each number corresponds to a specific configuration,
|
||||
aiding users in identifying and adjusting the settings used within each
|
||||
selection. For instance, when selecting "8 - Numeric State - Above" as
|
||||
a dropdown option, settings marked "Used in options 8, 9 or 10" are required
|
||||
for that respective selection because number 8 is included in those options.
|
||||
|
||||
'
|
||||
default: button_any_state
|
||||
selector:
|
||||
select:
|
||||
custom_value: true
|
||||
mode: dropdown
|
||||
options:
|
||||
- label: 1 - Button Or Any State
|
||||
value: button_any_state
|
||||
- label: 2 - ON
|
||||
value: 'on'
|
||||
- label: 3 - OFF
|
||||
value: 'off'
|
||||
- label: 4 - Unavailable
|
||||
value: unavailable
|
||||
- label: 5 - Unknown
|
||||
value: unknown
|
||||
- label: 6 - Home
|
||||
value: home
|
||||
- label: 7 - Not Home
|
||||
value: not_home
|
||||
- label: 8 - Numeric State - Above
|
||||
value: numeric_state_above
|
||||
- label: 9 - Numeric State - Below
|
||||
value: numeric_state_below
|
||||
- label: 10 - Numeric State - Above & Below
|
||||
value: numeric_state_above_below
|
||||
sort: false
|
||||
multiple: false
|
||||
trigger_state_entity:
|
||||
name: State Entities
|
||||
description: '**Used in options 1, 2, 3, 4, 5, 6 or 7**. Enter the entities
|
||||
that will trigger the automation on state change.
|
||||
|
||||
'
|
||||
default: []
|
||||
selector:
|
||||
entity:
|
||||
multiple: true
|
||||
trigger_numeric_entity:
|
||||
name: Numeric State Entities
|
||||
description: '**Used in options 8, 9 or 10**. Enter the entities that will
|
||||
trigger the automation numeric state change.
|
||||
|
||||
'
|
||||
default: []
|
||||
selector:
|
||||
entity:
|
||||
multiple: true
|
||||
above_state:
|
||||
name: Numeric State Above Option
|
||||
description: '**Used in options 8, or 10**. Set the above value. The numeric
|
||||
state trigger will be when the value crosses over to above the set value.
|
||||
|
||||
'
|
||||
default: 0
|
||||
selector:
|
||||
number:
|
||||
min: -20.0
|
||||
max: 100.0
|
||||
step: 1.0
|
||||
unit_of_measurement: value
|
||||
mode: slider
|
||||
below_state:
|
||||
name: Numeric State Below Option
|
||||
description: '**Used in options 9 or 10**. Set the below value. The numeric
|
||||
state trigger will be when the value crosses over to below the set value.
|
||||
|
||||
'
|
||||
default: 0
|
||||
selector:
|
||||
number:
|
||||
min: -20.0
|
||||
max: 100.0
|
||||
step: 1.0
|
||||
unit_of_measurement: value
|
||||
mode: slider
|
||||
time_delay_state:
|
||||
name: Time Delay
|
||||
description: '**Used in options 1, 2, 3, 4, 5, 6, 7, 8, 9 or 10** Specifies
|
||||
the duration that the trigger must maintain its state before the automation
|
||||
is executed. The automation will only proceed if the trigger condition
|
||||
is met continuously for this entire time delay.
|
||||
|
||||
'
|
||||
default:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 0
|
||||
selector:
|
||||
duration: {}
|
||||
device_notify_settings:
|
||||
name: Mobile App Notify
|
||||
icon: mdi:devices
|
||||
collapsed: true
|
||||
input:
|
||||
include_notify:
|
||||
name: Use The Mobile App Notify Option (Optional)
|
||||
description: "Enabling this option will send notifications to the selected
|
||||
devices below. \n"
|
||||
default: disable_mobile_app_notify
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: Enable Mobile App Notify Options
|
||||
value: enable_mobile_app_notify
|
||||
- label: Disable Mobile App Notify Options
|
||||
value: disable_mobile_app_notify
|
||||
custom_value: false
|
||||
sort: false
|
||||
multiple: false
|
||||
notify_device:
|
||||
name: Devices Notified
|
||||
description: 'Select the devices to be notified when the automation is triggered.
|
||||
|
||||
'
|
||||
default: []
|
||||
selector:
|
||||
device:
|
||||
filter:
|
||||
- integration: mobile_app
|
||||
multiple: true
|
||||
notify_title:
|
||||
name: Title
|
||||
description: 'Enter in the title of your notification.
|
||||
|
||||
'
|
||||
default: "\U0001F4E2 Enter Notify Title Here"
|
||||
selector:
|
||||
text: {}
|
||||
notify_message:
|
||||
name: Message
|
||||
description: 'Enter in the message of your notification.
|
||||
|
||||
'
|
||||
default: "Enter Notify Message Here \U0001F642"
|
||||
selector:
|
||||
text: {}
|
||||
notify_interruption_level:
|
||||
name: Interruption Level - iOS Only
|
||||
description: 'On devices running iOS 15 and later, you can configure the
|
||||
interruption level for your notifications to ensure they are delivered
|
||||
according to your preferences. Choose the desired interruption level from
|
||||
the dropdown list. Critical and time-sensitive notifications must be turned
|
||||
ON within the Home Assistant App, and time-sensitive notifications must
|
||||
be allowed in your Focus settings.
|
||||
|
||||
|
||||
For more information on interruption levels [Click Here](https://community.home-assistant.io/t/728100/13)
|
||||
|
||||
'
|
||||
default: active
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
options:
|
||||
- label: Default
|
||||
value: active
|
||||
- label: Critical Notifications
|
||||
value: critical
|
||||
- label: Time Sensitive Notifications
|
||||
value: time-sensitive
|
||||
- label: Quiet Notifications Without Waking Screen
|
||||
value: passive
|
||||
custom_value: false
|
||||
sort: false
|
||||
multiple: false
|
||||
notify_data:
|
||||
name: Android Only Options (Optional)
|
||||
description: '**High Priority** notifications ensure that you receive important
|
||||
alerts immediately. These notifications bypass normal delivery delays
|
||||
and are delivered instantly to your device.
|
||||
|
||||
|
||||
**Sticky Notification** ensure that important alerts are not dismissed
|
||||
when selected. They remain on your notification screen until you actively
|
||||
clear or swipe them away, ensuring that critical information stays visible
|
||||
until addressed.
|
||||
|
||||
|
||||
**Notification Channel** allow you to easily organize different notification
|
||||
settings, including notification sounds, vibrations, and other device-specific
|
||||
features. If you choose this option, please enter your desired channel
|
||||
name below.
|
||||
|
||||
'
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
options:
|
||||
- label: High Priority
|
||||
value: high_priority
|
||||
- label: Sticky Notification
|
||||
value: sticky
|
||||
- label: Notification Channel
|
||||
value: channel
|
||||
custom_value: false
|
||||
sort: false
|
||||
notify_channel:
|
||||
name: Notification Channel - Android Only
|
||||
description: 'If you have chosen to use a notification channel, simply enter
|
||||
the name of an existing channel to apply its pre-configured settings.
|
||||
If you don''t have a channel yet, you can create a new one by entering
|
||||
a name here. When your automation sends the first notification, it will
|
||||
create the channel on your device. After the channel is created, you can
|
||||
customize its notification settings to your preference directly on your
|
||||
device.
|
||||
|
||||
|
||||
For more information on notification channels [Click Here](https://community.home-assistant.io/t/728100/13)
|
||||
|
||||
'
|
||||
default: []
|
||||
selector:
|
||||
text: {}
|
||||
persistent_notify_settings:
|
||||
name: Notifications
|
||||
icon: mdi:bell-outline
|
||||
collapsed: true
|
||||
input:
|
||||
include_persistent_notification:
|
||||
name: Use The UI Notification Option (Optional)
|
||||
description: "Enabling this option will display notifications directly within
|
||||
the Home Assistant user interface (UI). \n"
|
||||
default: disabled_persistent_notification
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: Enable UI Notifications
|
||||
value: enable_persistent_notification
|
||||
- label: Disable UI Notifications
|
||||
value: disabled_persistent_notification
|
||||
custom_value: false
|
||||
sort: false
|
||||
multiple: false
|
||||
persistent_title:
|
||||
name: Title
|
||||
description: 'Enter in the title of your UI notification.
|
||||
|
||||
'
|
||||
default: "\U0001F4E2 Enter UI Title Here"
|
||||
selector:
|
||||
text: {}
|
||||
persistent_message:
|
||||
name: Message
|
||||
description: 'Enter in the message of your UI notification.
|
||||
|
||||
'
|
||||
default: "Enter UI Message Here \U0001F642"
|
||||
selector:
|
||||
text: {}
|
||||
tts_notify_settings:
|
||||
name: Text-to-Speech Announcement
|
||||
icon: mdi:bullhorn-outline
|
||||
collapsed: true
|
||||
input:
|
||||
include_tts_announcement:
|
||||
name: Use The Text-to-Speech Announcement Option (Optional)
|
||||
description: 'Enabling this option will send a Text-to-Speech notification
|
||||
to the selected Media Players below.
|
||||
|
||||
'
|
||||
default: disable_tts_announcement
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: Enable Text-to-Speech Announcements
|
||||
value: enable_tts_announcement
|
||||
- label: Disable Text-to-Speech Announcements
|
||||
value: disable_tts_announcement
|
||||
custom_value: false
|
||||
sort: false
|
||||
multiple: false
|
||||
text_to_speech_engine:
|
||||
name: Text-to-Speech Service Provider
|
||||
description: 'Select the Text-to-Speech service provider.
|
||||
|
||||
'
|
||||
default: []
|
||||
selector:
|
||||
entity:
|
||||
domain:
|
||||
- tts
|
||||
multiple: false
|
||||
media_player:
|
||||
name: Media Player
|
||||
description: 'Select the Media Player/s that will play your Text-to-Speech
|
||||
announcement message.
|
||||
|
||||
'
|
||||
default: []
|
||||
selector:
|
||||
entity:
|
||||
domain:
|
||||
- media_player
|
||||
multiple: true
|
||||
notify_tts_message:
|
||||
name: Announcement Message
|
||||
description: 'Enter in the message of your Text-to-Speech announcement.
|
||||
|
||||
'
|
||||
default: Enter TTS Message Here
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
multiple: false
|
||||
time_settings:
|
||||
name: Time
|
||||
icon: mdi:clock-outline
|
||||
collapsed: true
|
||||
input:
|
||||
include_time:
|
||||
name: Use The Time Options (Optional)
|
||||
description: 'Use the "Start Time", **End Time** and the **Weekdays** values
|
||||
to only run the automation between the time periods. This option allows
|
||||
you to disable the automation during sleeping hours to avoid disturbing
|
||||
anyone. Another example is setting the time between 5pm and 9pm for the
|
||||
''Dinner is Ready'' call. This way, if you accidentally activate the trigger
|
||||
outside these hours, the automation will not run.
|
||||
|
||||
'
|
||||
default: time_disabled
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: Enable the time options
|
||||
value: time_enabled
|
||||
- label: Disable the time options
|
||||
value: time_disabled
|
||||
custom_value: false
|
||||
sort: false
|
||||
multiple: false
|
||||
after_time:
|
||||
name: Start Time
|
||||
description: 'Set the start time.
|
||||
|
||||
'
|
||||
default: 00:00:00
|
||||
selector:
|
||||
time: {}
|
||||
before_time:
|
||||
name: End Time
|
||||
description: 'Set the end time.
|
||||
|
||||
'
|
||||
default: 00:00:00
|
||||
selector:
|
||||
time: {}
|
||||
weekday_options:
|
||||
name: Weekdays
|
||||
description: 'Select the days of the week you would like the automation
|
||||
to run. You must select "Enable the time options" above for the weekdays
|
||||
selections to work.
|
||||
|
||||
'
|
||||
default:
|
||||
- mon
|
||||
- tue
|
||||
- wed
|
||||
- thu
|
||||
- fri
|
||||
- sat
|
||||
- sun
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
mode: list
|
||||
options:
|
||||
- label: Monday
|
||||
value: mon
|
||||
- label: Tuesday
|
||||
value: tue
|
||||
- label: Wednesday
|
||||
value: wed
|
||||
- label: Thursday
|
||||
value: thu
|
||||
- label: Friday
|
||||
value: fri
|
||||
- label: Saturday
|
||||
value: sat
|
||||
- label: Sunday
|
||||
value: sun
|
||||
custom_value: false
|
||||
sort: false
|
||||
global_conditions_settings:
|
||||
name: Global Conditions
|
||||
icon: mdi:earth
|
||||
collapsed: true
|
||||
input:
|
||||
global_conditions:
|
||||
name: Global Conditions
|
||||
description: 'Enter any global conditions you would like to apply to the
|
||||
automation.
|
||||
|
||||
'
|
||||
default: []
|
||||
selector:
|
||||
condition: {}
|
||||
source_url: https://gist.github.com/Blackshome/180ca4a24af81cd5d843acfc039039bc
|
||||
mode: single
|
||||
max_exceeded: silent
|
||||
variables:
|
||||
trigger_state: !input trigger_state
|
||||
trigger_state_entity: !input trigger_state_entity
|
||||
trigger_numeric_entity: !input trigger_numeric_entity
|
||||
above_state: !input above_state
|
||||
below_state: !input below_state
|
||||
time_delay_state: !input time_delay_state
|
||||
include_notify: !input include_notify
|
||||
notify_device: !input notify_device
|
||||
notify_title: !input notify_title
|
||||
notify_message: !input notify_message
|
||||
notify_interruption_level: !input notify_interruption_level
|
||||
notify_data: !input notify_data
|
||||
notify_channel: !input notify_channel
|
||||
include_persistent_notification: !input include_persistent_notification
|
||||
persistent_title: !input persistent_title
|
||||
persistent_message: !input persistent_message
|
||||
include_tts_announcement: !input include_tts_announcement
|
||||
text_to_speech_engine: !input text_to_speech_engine
|
||||
media_player: !input media_player
|
||||
notify_tts_message: !input notify_tts_message
|
||||
include_time: !input include_time
|
||||
after_time: !input after_time
|
||||
before_time: !input before_time
|
||||
weekday_options: !input weekday_options
|
||||
global_conditions: !input global_conditions
|
||||
device_message_data: "{% set message = namespace(data={}) %} {% if notify_interruption_level
|
||||
in ['active', 'critical', 'time-sensitive', 'passive'] %}\n {% set message.data
|
||||
= dict(message.data, **{ 'push': {'interruption-level': notify_interruption_level
|
||||
}}) %}\n{% endif %} {% if 'high_priority' in notify_data %}\n {% set message.data
|
||||
= dict(message.data, **{ 'ttl': 0, 'priority': 'high' }) %}\n{% endif %} {% if
|
||||
'channel' in notify_data %}\n {% set message.data = dict(message.data, **{ 'channel':
|
||||
notify_channel }) %}\n{% endif %} {% if 'sticky' in notify_data %}\n {% set message.data
|
||||
= dict(message.data, **{ 'sticky': \"true\" }) %}\n{% endif %} {{ message.data
|
||||
}}"
|
||||
trigger:
|
||||
- platform: state
|
||||
id: t0
|
||||
entity_id: !input trigger_state_entity
|
||||
for: !input time_delay_state
|
||||
- platform: state
|
||||
id: t1
|
||||
entity_id: !input trigger_state_entity
|
||||
to: !input trigger_state
|
||||
for: !input time_delay_state
|
||||
- platform: numeric_state
|
||||
id: t2
|
||||
entity_id: !input trigger_numeric_entity
|
||||
above: !input above_state
|
||||
for: !input time_delay_state
|
||||
- platform: numeric_state
|
||||
id: t3
|
||||
entity_id: !input trigger_numeric_entity
|
||||
below: !input below_state
|
||||
for: !input time_delay_state
|
||||
condition:
|
||||
- condition: or
|
||||
conditions:
|
||||
- condition: and
|
||||
conditions:
|
||||
- condition: trigger
|
||||
id: t0
|
||||
- '{{ trigger_state == ''button_any_state'' }}'
|
||||
- condition: and
|
||||
conditions:
|
||||
- condition: trigger
|
||||
id: t1
|
||||
- '{{ (trigger_state != ''button_any_state'') or (trigger_state != ''numeric_state_above'')
|
||||
or (trigger_state != ''numeric_state_below'') or (trigger_state != ''numeric_state_above_below'')
|
||||
}}'
|
||||
- condition: and
|
||||
conditions:
|
||||
- condition: trigger
|
||||
id: t2
|
||||
- '{{ (trigger_state == ''numeric_state_above'') or (trigger_state == ''numeric_state_above_below'')
|
||||
}}'
|
||||
- condition: and
|
||||
conditions:
|
||||
- condition: trigger
|
||||
id: t3
|
||||
- '{{ (trigger_state == ''numeric_state_below'') or (trigger_state == ''numeric_state_above_below'')
|
||||
}}'
|
||||
- condition: or
|
||||
conditions:
|
||||
- '{{ include_time == ''time_disabled'' }}'
|
||||
- condition: and
|
||||
conditions:
|
||||
- condition: time
|
||||
after: !input after_time
|
||||
before: !input before_time
|
||||
weekday: !input weekday_options
|
||||
- '{{ include_time == ''time_enabled'' }}'
|
||||
- condition: and
|
||||
conditions: !input global_conditions
|
||||
action:
|
||||
- choose:
|
||||
- alias: Check if notification is enabled
|
||||
conditions:
|
||||
- '{{ include_notify == ''enable_mobile_app_notify'' }}'
|
||||
sequence:
|
||||
- alias: Send a notification to each device
|
||||
repeat:
|
||||
for_each: !input notify_device
|
||||
sequence:
|
||||
- service: notify.mobile_app_{{ device_attr(repeat.item, 'name') | slugify
|
||||
}}
|
||||
data:
|
||||
title: !input notify_title
|
||||
message: !input notify_message
|
||||
data: '{{ device_message_data }}'
|
||||
- choose:
|
||||
- alias: Use the persistent notification is enabled
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: '{{ include_persistent_notification == ''enable_persistent_notification''
|
||||
}}'
|
||||
sequence:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: !input persistent_title
|
||||
message: !input persistent_message
|
||||
- choose:
|
||||
- alias: Check if tts announcement is enabled
|
||||
conditions:
|
||||
- '{{ include_tts_announcement == ''enable_tts_announcement'' }}'
|
||||
sequence:
|
||||
- service: tts.speak
|
||||
target:
|
||||
entity_id: !input text_to_speech_engine
|
||||
data:
|
||||
media_player_entity_id: !input media_player
|
||||
message: !input notify_tts_message
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user