core/homeassistant/components/knx/project.py

148 lines
4.7 KiB
Python

"""Handle KNX project data."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from typing import Final
from xknx import XKNX
from xknx.dpt import DPTBase
from xknx.telegram.address import DeviceAddressableType, GroupAddress, GroupAddressType
from xknxproject import XKNXProj
from xknxproject.models import (
Device,
DPTType,
GroupAddress as GroupAddressModel,
GroupAddressStyle as XknxProjectGroupAddressStyle,
KNXProject as KNXProjectModel,
ProjectInfo,
)
from homeassistant.components.file_upload import process_uploaded_file
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.storage import Store
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STORAGE_VERSION: Final = 1
STORAGE_KEY: Final = f"{DOMAIN}/knx_project.json"
@dataclass
class GroupAddressInfo:
"""Group address info for runtime usage."""
address: str
name: str
description: str
dpt_main: int | None
dpt_sub: int | None
transcoder: type[DPTBase] | None
def _create_group_address_info(ga_model: GroupAddressModel) -> GroupAddressInfo:
"""Convert GroupAddress dict value into GroupAddressInfo instance."""
dpt = ga_model["dpt"]
transcoder = DPTBase.transcoder_by_dpt(dpt["main"], dpt.get("sub")) if dpt else None
return GroupAddressInfo(
address=ga_model["address"],
name=ga_model["name"],
description=ga_model["description"],
transcoder=transcoder,
dpt_main=dpt["main"] if dpt else None,
dpt_sub=dpt["sub"] if dpt else None,
)
class KNXProject:
"""Manage KNX project data."""
loaded: bool
devices: dict[str, Device]
group_addresses: dict[str, GroupAddressInfo]
info: ProjectInfo | None
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
) -> None:
"""Initialize project data."""
self.hass = hass
self._store = Store[KNXProjectModel](hass, STORAGE_VERSION, STORAGE_KEY)
self.initial_state()
def initial_state(self) -> None:
"""Set initial state for project data."""
self.loaded = False
self.devices = {}
self.group_addresses = {}
self.info = None
async def load_project(
self, xknx: XKNX, data: KNXProjectModel | None = None
) -> None:
"""Load project data from storage."""
if project := data or await self._store.async_load():
self.devices = project["devices"]
self.info = project["info"]
GroupAddress.address_format = self.get_address_format()
xknx.group_address_dpt.clear()
xknx_ga_dict: dict[DeviceAddressableType, DPTType] = {}
for ga_model in project["group_addresses"].values():
ga_info = _create_group_address_info(ga_model)
self.group_addresses[ga_info.address] = ga_info
if (dpt_model := ga_model.get("dpt")) is not None:
xknx_ga_dict[ga_model["address"]] = dpt_model
xknx.group_address_dpt.set(xknx_ga_dict)
_LOGGER.debug(
"Loaded KNX project data with %s group addresses from storage",
len(self.group_addresses),
)
self.loaded = True
async def process_project_file(
self, xknx: XKNX, file_id: str, password: str
) -> None:
"""Process an uploaded project file."""
def _parse_project() -> KNXProjectModel:
with process_uploaded_file(self.hass, file_id) as file_path:
xknxproj = XKNXProj(
file_path,
password=password,
language=self.hass.config.language,
)
return xknxproj.parse()
project = await self.hass.async_add_executor_job(_parse_project)
await self._store.async_save(project)
await self.load_project(xknx, data=project)
async def remove_project_file(self) -> None:
"""Remove project file from storage."""
await self._store.async_remove()
self.initial_state()
async def get_knxproject(self) -> KNXProjectModel | None:
"""Load the project file from local storage."""
return await self._store.async_load()
def get_address_format(self) -> GroupAddressType:
"""Return the address format for group addresses used in the project."""
if self.info:
match self.info["group_address_style"]:
case XknxProjectGroupAddressStyle.TWOLEVEL.value:
return GroupAddressType.SHORT
case XknxProjectGroupAddressStyle.FREE.value:
return GroupAddressType.FREE
return GroupAddressType.LONG