chore: initial baseline before refactoring
Initial snapshot of Home Assistant config (HAOS 2026.4.4)
prior to dashboard restructuring and helper YAML migration.
Pre-commit cleanup applied:
- Removed blueprints/switch_manager/ (9.4 MB stale, component already gone)
- Removed .storage/switch_manager
- Removed home-assistant.log.{1,old,fault}
- Removed zigbee2mqtt/configuration_backup_v{1..4}.yaml
- Removed zigbee2mqtt/database.db.tmp.2024-09-27 (1.5y old)
- Created empty themes/ to satisfy configuration.yaml include
.gitignore uses allowlist strategy for .storage/ to keep
all tokens, keys, and PINs out of version control.
This commit is contained in:
@@ -0,0 +1,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)
|
||||
Reference in New Issue
Block a user