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:
2026-05-02 14:24:53 +02:00
commit 5f01411780
149 changed files with 47886 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
2026.4.4
+149
View File
@@ -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.
# ============================================================
+256
View File
@@ -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"
}
]
}
}
+46
View File
@@ -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"
}
]
}
}
}
+27
View File
@@ -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"
}
]
}
}
+63
View File
@@ -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"
}
]
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"version": 1,
"minor_version": 1,
"key": "counter",
"data": {
"items": []
}
}
+98
View File
@@ -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": []
}
}
+13
View File
@@ -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
+22
View File
@@ -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"
}
]
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"version": 1,
"minor_version": 1,
"key": "input_button",
"data": {
"items": [
{
"id": "nachtmodus",
"name": "Nachtmodus",
"icon": "mdi:weather-night"
}
]
}
}
+15
View File
@@ -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
}
]
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"version": 1,
"minor_version": 2,
"key": "input_select",
"data": {
"items": []
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"version": 1,
"minor_version": 1,
"key": "input_text",
"data": {
"items": []
}
}
+535
View File
@@ -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
+498
View File
@@ -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 12× 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 12× 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 %} 34 Tabletten in Dosierschwimmer. {% else %} 12 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,07,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,07,6) | ② Brom-Tabletten im Dosierschwimmer prüfen & ggf. nachlegen | ③ 10 ml Wasserrein (Bayrol) zugeben | ④ Pumpe mind. 30 min laufen lassen | ⑤ Filtration läuft? (mind. 46h/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
+12
View File
@@ -0,0 +1,12 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.map",
"data": {
"config": {
"strategy": {
"type": "map"
}
}
}
}
+54
View File
@@ -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"
}
]
}
}
+104
View File
@@ -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"
}
]
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"version": 1,
"minor_version": 1,
"key": "map",
"data": {
"migrated": true
}
}
+21
View File
@@ -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}}
}
}
}
+56
View File
@@ -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"
}
]
}
]
}
}
+91
View File
@@ -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": []
}
}
+15
View File
@@ -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
}
]
}
}
+275
View File
@@ -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
+39
View File
@@ -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 *
+150
View File
@@ -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
+33
View File
@@ -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]
+1
View File
@@ -0,0 +1 @@
__version__ = "v5.2.3"
+386
View File
@@ -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