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 Inventory(mashumaro.mixins.yaml.DataClassYAMLMixin):
116@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>)
language: str | None = None

The default system language.

areas: list[Area]

The list of areas within the home.

devices: list[Device]

The list of devices within a home, which may be associated with areas.

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.

def device_dict(self) -> dict[str, Device]:
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.

def area_dict(self) -> dict[str, Area]:
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}

Dictionary of areas by area id.

def to_dict(self, *, omit_none=False):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

class Inventory.Config(mashumaro.config.BaseConfig):
149    class Config(BaseConfig):
150        code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"]
151        sort_keys = False
code_generation_options = ['TO_DICT_ADD_OMIT_NONE_FLAG']
sort_keys = False
def load_inventory(config_file: pathlib._local.Path) -> Inventory:
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 Area(mashumaro.mixins.yaml.DataClassYAMLMixin):
35@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.

Area(name: str, id: str | None = None, floor: str | None = None)
name: str

The human readable name of the area e.g. 'Living Room'.

id: str | None = None

The identifier of the area eg. 'living_room', unique within a Home.

floor: str | None = None

The human readable name of the floor.

def to_dict(self, *, omit_none=False):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

class Area.Config(mashumaro.config.BaseConfig):
53    class Config(BaseConfig):
54        code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"]
55        sort_keys = False
code_generation_options = ['TO_DICT_ADD_OMIT_NONE_FLAG']
sort_keys = False
@dataclass
class Device(mashumaro.mixins.yaml.DataClassYAMLMixin):
58@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)
name: str

The human readable name of the device e.g. 'Left Blind'.

id: str | None = None

The identifier of the area eg. 'living_room_left_blind', unique within a Home.

area: str | None = None

The area id of the home. e.g. 'living_room'

info: synthetic_home.common.DeviceInfo | None = None

Detailed model information about the device.

def to_dict(self, *, omit_none=False):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

class Device.Config(mashumaro.config.BaseConfig):
74    class Config(BaseConfig):
75        code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"]
76        sort_keys = False
code_generation_options = ['TO_DICT_ADD_OMIT_NONE_FLAG']
sort_keys = False
@dataclass
class Entity(mashumaro.mixins.yaml.DataClassYAMLMixin):
 84@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.

area: str | None = None

The area id within the home e.g. 'living_room'.

device: str | None = None

The device id within the home e.g. 'living_room_left_blind'.

state: str | None = None

The current state value for the entity.

attributes: dict[str, str | int | float | bool | list[dict[str, str | int | float | bool]] | list[str | int | float | bool]] | None = None

The current state attributes for the entity.

def to_dict(self, *, omit_none=False):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

class Entity.Config(mashumaro.config.BaseConfig):
111    class Config(BaseConfig):
112        code_generation_options = ["TO_DICT_ADD_OMIT_NONE_FLAG"]
113        sort_keys = False
code_generation_options = ['TO_DICT_ADD_OMIT_NONE_FLAG']
sort_keys = False