synthetic_home.inventory
Data model for the lower level inventory of a home, usable for fixtures and evaluations.
1"""Data model for the lower level inventory of a home, usable for fixtures and evaluations.""" 2 3import pathlib 4from dataclasses import dataclass, field 5import logging 6import slugify 7from typing import Any 8 9import yaml 10from mashumaro.mixins.yaml import DataClassYAMLMixin, EncodedData 11from mashumaro.config import BaseConfig 12from mashumaro.codecs.yaml import yaml_decode 13 14from . import common 15from .exceptions import SyntheticHomeError 16 17__all__ = [ 18 "Inventory", 19 "load_inventory", 20 "Area", 21 "Device", 22 "Entity", 23] 24 25_LOGGER = logging.getLogger(__name__) 26 27DEFAULT_SEPARATOR = "_" 28 29 30def _custom_encoder(data: dict[str, Any]) -> EncodedData: 31 return yaml.safe_dump(data, sort_keys=False, explicit_start=True) # type: ignore[no-any-return] 32 33 34@dataclass 35class Area(DataClassYAMLMixin): 36 """Represents an area in a home.""" 37 38 name: str 39 """The human readable name of the area e.g. 'Living Room'.""" 40 41 id: str | None = None 42 """The identifier of the area eg. 'living_room', unique within a Home.""" 43 44 floor: str | None = None 45 """The human readable name of the floor.""" 46 47 def __post_init__(self) -> None: 48 """Validate the area.""" 49 if self.id is None: 50 self.id = slugify.slugify(self.name, separator=DEFAULT_SEPARATOR) 51 52 class Config(BaseConfig): 53 code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"] 54 sort_keys = False 55 56 57@dataclass 58class Device(DataClassYAMLMixin): 59 """Represents a devie within a home.""" 60 61 name: str 62 """The human readable name of the device e.g. 'Left Blind'.""" 63 64 id: str | None = None 65 """The identifier of the area eg. 'living_room_left_blind', unique within a Home.""" 66 67 area: str | None = None 68 """The area id of the home. e.g. 'living_room'""" 69 70 info: common.DeviceInfo | None = None 71 """Detailed model information about the device.""" 72 73 class Config(BaseConfig): 74 code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"] 75 sort_keys = False 76 77 def __post_init__(self) -> None: 78 """Validate the device.""" 79 if self.id is None: 80 self.id = slugify.slugify(self.name, separator=DEFAULT_SEPARATOR) 81 82 83@dataclass 84class Entity(DataClassYAMLMixin): 85 """Represents an entity within a home.""" 86 87 name: str | None = None 88 """The human readable name of the entity e.g. 'Outside rain sensor'. or omitted to use device naming.""" 89 90 id: str | None = None 91 """The identifier of the entity e.g. 'sensor.rain_sensor_intensity', unique within a Home.""" 92 93 area: str | None = None 94 """The area id within the home e.g. 'living_room'.""" 95 96 device: str | None = None 97 """The device id within the home e.g. 'living_room_left_blind'.""" 98 99 state: str | None = None 100 """The current state value for the entity.""" 101 102 attributes: common.NamedAttributes | None = None 103 """The current state attributes for the entity.""" 104 105 def __post_init__(self) -> None: 106 """Validate the device.""" 107 if self.id is None: 108 raise SyntheticHomeError("Entity {self.name} had no value for 'id'") 109 110 class Config(BaseConfig): 111 code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"] 112 sort_keys = False 113 114 115@dataclass 116class Inventory(DataClassYAMLMixin): 117 """A fixture defines the entire definition of the home.""" 118 119 language: str | None = None 120 """The default system language.""" 121 122 areas: list[Area] = field(default_factory=list) 123 """The list of areas within the home.""" 124 125 devices: list[Device] = field(default_factory=list) 126 """The list of devices within a home, which may be associated with areas.""" 127 128 entities: list[Entity] = field(default_factory=list) 129 """The list of entities within a home, which may be associated with areas and devices.""" 130 131 def yaml(self) -> str: 132 """Render the inventory as yaml.""" 133 return str(self.to_yaml(omit_none=True, encoder=_custom_encoder)) 134 135 @property 136 def floors(self) -> set[str]: 137 """Return the set of floors across all areas.""" 138 return {area.floor for area in self.areas if area.floor is not None} 139 140 def device_dict(self) -> dict[str, Device]: 141 """Dictionary of devices by device id.""" 142 return {device.id: device for device in self.devices if device.id is not None} 143 144 def area_dict(self) -> dict[str, Area]: 145 """Dictionary of areas by area id.""" 146 return {area.id: area for area in self.areas if area.id is not None} 147 148 class Config(BaseConfig): 149 code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"] 150 sort_keys = False 151 152 153def read_config_content(config_file: pathlib.Path) -> str: 154 """Create configuration file content, exposed for patching.""" 155 with config_file.open("r") as f: 156 return f.read() 157 158 159def decode_inventory(content: str) -> Inventory: 160 """Load synthetic home configuration from disk.""" 161 return yaml_decode(content, Inventory) 162 163 164def load_inventory(config_file: pathlib.Path) -> Inventory: 165 """Load synthetic home configuration from disk.""" 166 try: 167 content = read_config_content(config_file) 168 except FileNotFoundError: 169 raise SyntheticHomeError(f"Configuration file '{config_file}' does not exist") 170 try: 171 return decode_inventory(content) 172 except ValueError as err: 173 raise SyntheticHomeError(f"Could not parse config file '{config_file}': {err}")
@dataclass
class
Inventory116@dataclass 117class Inventory(DataClassYAMLMixin): 118 """A fixture defines the entire definition of the home.""" 119 120 language: str | None = None 121 """The default system language.""" 122 123 areas: list[Area] = field(default_factory=list) 124 """The list of areas within the home.""" 125 126 devices: list[Device] = field(default_factory=list) 127 """The list of devices within a home, which may be associated with areas.""" 128 129 entities: list[Entity] = field(default_factory=list) 130 """The list of entities within a home, which may be associated with areas and devices.""" 131 132 def yaml(self) -> str: 133 """Render the inventory as yaml.""" 134 return str(self.to_yaml(omit_none=True, encoder=_custom_encoder)) 135 136 @property 137 def floors(self) -> set[str]: 138 """Return the set of floors across all areas.""" 139 return {area.floor for area in self.areas if area.floor is not None} 140 141 def device_dict(self) -> dict[str, Device]: 142 """Dictionary of devices by device id.""" 143 return {device.id: device for device in self.devices if device.id is not None} 144 145 def area_dict(self) -> dict[str, Area]: 146 """Dictionary of areas by area id.""" 147 return {area.id: area for area in self.areas if area.id is not None} 148 149 class Config(BaseConfig): 150 code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"] 151 sort_keys = False
A fixture defines the entire definition of the home.
Inventory( language: str | None = None, areas: list[Area] = <factory>, devices: list[Device] = <factory>, entities: list[Entity] = <factory>)
entities: list[Entity]
The list of entities within a home, which may be associated with areas and devices.
def
yaml(self) -> str:
132 def yaml(self) -> str: 133 """Render the inventory as yaml.""" 134 return str(self.to_yaml(omit_none=True, encoder=_custom_encoder))
Render the inventory as yaml.
floors: set[str]
136 @property 137 def floors(self) -> set[str]: 138 """Return the set of floors across all areas.""" 139 return {area.floor for area in self.areas if area.floor is not None}
Return the set of floors across all areas.
141 def device_dict(self) -> dict[str, Device]: 142 """Dictionary of devices by device id.""" 143 return {device.id: device for device in self.devices if device.id is not None}
Dictionary of devices by device id.
class
Inventory.Config(mashumaro.config.BaseConfig):
165def load_inventory(config_file: pathlib.Path) -> Inventory: 166 """Load synthetic home configuration from disk.""" 167 try: 168 content = read_config_content(config_file) 169 except FileNotFoundError: 170 raise SyntheticHomeError(f"Configuration file '{config_file}' does not exist") 171 try: 172 return decode_inventory(content) 173 except ValueError as err: 174 raise SyntheticHomeError(f"Could not parse config file '{config_file}': {err}")
Load synthetic home configuration from disk.
@dataclass
class
Area35@dataclass 36class Area(DataClassYAMLMixin): 37 """Represents an area in a home.""" 38 39 name: str 40 """The human readable name of the area e.g. 'Living Room'.""" 41 42 id: str | None = None 43 """The identifier of the area eg. 'living_room', unique within a Home.""" 44 45 floor: str | None = None 46 """The human readable name of the floor.""" 47 48 def __post_init__(self) -> None: 49 """Validate the area.""" 50 if self.id is None: 51 self.id = slugify.slugify(self.name, separator=DEFAULT_SEPARATOR) 52 53 class Config(BaseConfig): 54 code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"] 55 sort_keys = False
Represents an area in a home.
class
Area.Config(mashumaro.config.BaseConfig):
@dataclass
class
Device58@dataclass 59class Device(DataClassYAMLMixin): 60 """Represents a devie within a home.""" 61 62 name: str 63 """The human readable name of the device e.g. 'Left Blind'.""" 64 65 id: str | None = None 66 """The identifier of the area eg. 'living_room_left_blind', unique within a Home.""" 67 68 area: str | None = None 69 """The area id of the home. e.g. 'living_room'""" 70 71 info: common.DeviceInfo | None = None 72 """Detailed model information about the device.""" 73 74 class Config(BaseConfig): 75 code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"] 76 sort_keys = False 77 78 def __post_init__(self) -> None: 79 """Validate the device.""" 80 if self.id is None: 81 self.id = slugify.slugify(self.name, separator=DEFAULT_SEPARATOR)
Represents a devie within a home.
Device( name: str, id: str | None = None, area: str | None = None, info: synthetic_home.common.DeviceInfo | None = None)
class
Device.Config(mashumaro.config.BaseConfig):
@dataclass
class
Entity84@dataclass 85class Entity(DataClassYAMLMixin): 86 """Represents an entity within a home.""" 87 88 name: str | None = None 89 """The human readable name of the entity e.g. 'Outside rain sensor'. or omitted to use device naming.""" 90 91 id: str | None = None 92 """The identifier of the entity e.g. 'sensor.rain_sensor_intensity', unique within a Home.""" 93 94 area: str | None = None 95 """The area id within the home e.g. 'living_room'.""" 96 97 device: str | None = None 98 """The device id within the home e.g. 'living_room_left_blind'.""" 99 100 state: str | None = None 101 """The current state value for the entity.""" 102 103 attributes: common.NamedAttributes | None = None 104 """The current state attributes for the entity.""" 105 106 def __post_init__(self) -> None: 107 """Validate the device.""" 108 if self.id is None: 109 raise SyntheticHomeError("Entity {self.name} had no value for 'id'") 110 111 class Config(BaseConfig): 112 code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"] 113 sort_keys = False
Represents an entity within a home.
Entity( name: str | None = None, id: str | None = None, area: str | None = None, device: str | None = None, state: str | None = None, attributes: dict[str, str | int | float | bool | list[dict[str, str | int | float | bool]] | list[str | int | float | bool]] | None = None)
name: str | None =
None
The human readable name of the entity e.g. 'Outside rain sensor'. or omitted to use device naming.
id: str | None =
None
The identifier of the entity e.g. 'sensor.rain_sensor_intensity', unique within a Home.
class
Entity.Config(mashumaro.config.BaseConfig):