Skip to content

Base

Context

Bases: BaseModel

Represents the context of a Home Assistant event.

Source code in src/hassette/models/states/base.py
18
19
20
21
22
23
24
25
26
27
28
29
30
class Context(BaseModel):
    """Represents the context of a Home Assistant event."""

    model_config = ConfigDict(frozen=True)

    id: str | None = Field(default=None)
    """The context ID of the event."""

    parent_id: str | None = Field(default=None)
    """The parent context ID of the event, if any."""

    user_id: str | None = Field(default=None)
    """The user ID for who triggered the event."""

id: str | None = Field(default=None) class-attribute instance-attribute

The context ID of the event.

parent_id: str | None = Field(default=None) class-attribute instance-attribute

The parent context ID of the event, if any.

user_id: str | None = Field(default=None) class-attribute instance-attribute

The user ID for who triggered the event.

AttributesBase

Bases: BaseModel

Represents the attributes of a HomeAssistant state.

Source code in src/hassette/models/states/base.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class AttributesBase(BaseModel):
    """Represents the attributes of a HomeAssistant state."""

    model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True, coerce_numbers_to_str=True, frozen=True)

    icon: str | None = Field(default=None, repr=False)
    """The icon of the entity."""

    friendly_name: str | None = Field(default=None)
    """A friendly name for the entity."""

    device_class: str | None = Field(default=None)
    """The device class of the entity."""

    entity_id: list[str] | None = Field(default=None)
    """List of entity IDs if this is a group entity."""

    supported_features: int | float | None = Field(default=None)
    """Bitfield of supported features."""

    @property
    def extras(self) -> dict[str, Any]:
        """Integration-specific attributes not covered by the typed model."""
        return self.model_extra or {}

    def extra(self, key: str, default: Any = None) -> Any:
        """Get a single integration-specific attribute with a default."""
        return self.extras.get(key, default)

    def has_feature(self, flag: int) -> bool:
        """Check whether *flag* is set in :pyattr:`supported_features`."""
        if self.supported_features is None:
            return False
        return bool(int(self.supported_features) & flag)

icon: str | None = Field(default=None, repr=False) class-attribute instance-attribute

The icon of the entity.

friendly_name: str | None = Field(default=None) class-attribute instance-attribute

A friendly name for the entity.

device_class: str | None = Field(default=None) class-attribute instance-attribute

The device class of the entity.

entity_id: list[str] | None = Field(default=None) class-attribute instance-attribute

List of entity IDs if this is a group entity.

supported_features: int | float | None = Field(default=None) class-attribute instance-attribute

Bitfield of supported features.

extras: dict[str, Any] property

Integration-specific attributes not covered by the typed model.

extra(key: str, default: Any = None) -> Any

Get a single integration-specific attribute with a default.

Source code in src/hassette/models/states/base.py
58
59
60
def extra(self, key: str, default: Any = None) -> Any:
    """Get a single integration-specific attribute with a default."""
    return self.extras.get(key, default)

has_feature(flag: int) -> bool

Check whether flag is set in :pyattr:supported_features.

Source code in src/hassette/models/states/base.py
62
63
64
65
66
def has_feature(self, flag: int) -> bool:
    """Check whether *flag* is set in :pyattr:`supported_features`."""
    if self.supported_features is None:
        return False
    return bool(int(self.supported_features) & flag)

BaseState

Bases: BaseModel, Generic[StateValueT]

Represents a Home Assistant state object.

Source code in src/hassette/models/states/base.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
class BaseState(BaseModel, Generic[StateValueT]):
    """Represents a Home Assistant state object."""

    model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True, coerce_numbers_to_str=True, frozen=True)

    value_type: ClassVar[type | tuple[type, ...]] = (str, type(None))
    """The Python type of the state value, e.g. bool for BinarySensorState."""

    domain: str
    """The domain of the entity, e.g. 'light', 'sensor', etc."""

    entity_id: str = Field(...)
    """The full entity ID, e.g. 'light.living_room'."""

    last_changed: ZonedDateTime | None = Field(None)
    """Time the state changed in the state machine, not updated when only attributes change."""

    last_reported: ZonedDateTime | None = Field(None)
    """Time the state was written to the state machine, updated regardless of any changes to the state or
    state attributes.
    """

    last_updated: ZonedDateTime | None = Field(None)
    """Time the state or state attributes changed in the state machine, not updated if neither state nor state
    attributes changed.
    """

    context: Context = Field(repr=False)
    """The context of the state change."""

    is_unknown: bool = Field(default=False)
    """Whether the state is 'unknown'."""

    is_unavailable: bool = Field(default=False)
    """Whether the state is 'unavailable'."""

    value: StateValueT = Field(..., validation_alias=AliasChoices("state", "value"))
    """The state value, e.g. 'on', 'off', 23.5, etc."""

    attributes: AttributesBase = Field(...)
    """The attributes of the state."""

    @property
    def is_group(self) -> bool:
        """Whether this entity is a group entity (i.e. has multiple entity_ids)."""
        if not self.attributes:
            return False

        if not hasattr(self.attributes, "entity_id"):
            return False

        if not isinstance(self.attributes.entity_id, list):  # pyright: ignore[reportAttributeAccessIssue]
            return False

        return len(self.attributes.entity_id) > 1  # pyright: ignore[reportAttributeAccessIssue]

    @property
    def extras(self) -> dict[str, Any]:
        """Extra fields not covered by the typed state model."""
        return self.model_extra or {}

    def extra(self, key: str, default: Any = None) -> Any:
        """Get a single extra field with a default."""
        return self.extras.get(key, default)

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        with suppress(NoDomainAnnotationError):
            register_state_converter(cls, domain=cls.get_domain())

    @field_validator("last_changed", "last_reported", "last_updated", mode="before")
    @classmethod
    def _validate_datetime_fields(cls, value):
        if value is None:
            return None
        if isinstance(value, int | float):
            return convert_utc_timestamp_to_system_tz(value)
        if isinstance(value, str):
            # need to use OffsetDateTime since the value is +00:00, not Z or a timezone
            return convert_datetime_str_to_system_tz(value)

        return value

    @model_validator(mode="before")
    @classmethod
    def _validate_domain_and_state(cls, values):
        if not isinstance(values, dict):
            LOGGER.warning("Expected values to be a dict, got %s", type(values).__name__, stacklevel=2)
            return values

        values = dict(values)

        entity_id = values.get("entity_id")
        if entity_id:
            domain = entity_id.split(".")[0]
            values["domain"] = domain

        state = values.get("state")
        if state == "unknown":
            values["is_unknown"] = True
            values["state"] = state = None
        elif state == "unavailable":
            values["is_unavailable"] = True
            values["state"] = state = None

        try:
            values["state"] = TYPE_REGISTRY.convert(state, cls.value_type)
        except UnableToConvertValueError as e:
            LOGGER.error(
                "Unable to convert state value %r for entity %s: %s", state, values.get("entity_id"), e, stacklevel=2
            )
            raise

        return values

    @classmethod
    def get_domain(cls) -> str:
        """Returns the domain string for this state class, extracted from the domain field annotation."""

        fields = cls.model_fields
        domain_field = fields.get("domain")
        if not domain_field:
            raise NoDomainAnnotationError(cls)

        annotations = get_annotations(cls)
        annotation = annotations.get("domain")
        if annotation is None:
            raise NoDomainAnnotationError(cls)

        args = get_args(annotation)
        if not args:
            raise NoDomainAnnotationError(cls)

        domain = args[0]
        if not isinstance(domain, str):
            raise NoDomainAnnotationError(cls)

        return domain

value_type: type | tuple[type, ...] = (str, type(None)) class-attribute

The Python type of the state value, e.g. bool for BinarySensorState.

domain: str instance-attribute

The domain of the entity, e.g. 'light', 'sensor', etc.

entity_id: str = Field(...) class-attribute instance-attribute

The full entity ID, e.g. 'light.living_room'.

last_changed: ZonedDateTime | None = Field(None) class-attribute instance-attribute

Time the state changed in the state machine, not updated when only attributes change.

last_reported: ZonedDateTime | None = Field(None) class-attribute instance-attribute

Time the state was written to the state machine, updated regardless of any changes to the state or state attributes.

last_updated: ZonedDateTime | None = Field(None) class-attribute instance-attribute

Time the state or state attributes changed in the state machine, not updated if neither state nor state attributes changed.

context: Context = Field(repr=False) class-attribute instance-attribute

The context of the state change.

is_unknown: bool = Field(default=False) class-attribute instance-attribute

Whether the state is 'unknown'.

is_unavailable: bool = Field(default=False) class-attribute instance-attribute

Whether the state is 'unavailable'.

value: StateValueT = Field(..., validation_alias=(AliasChoices('state', 'value'))) class-attribute instance-attribute

The state value, e.g. 'on', 'off', 23.5, etc.

attributes: AttributesBase = Field(...) class-attribute instance-attribute

The attributes of the state.

is_group: bool property

Whether this entity is a group entity (i.e. has multiple entity_ids).

extras: dict[str, Any] property

Extra fields not covered by the typed state model.

extra(key: str, default: Any = None) -> Any

Get a single extra field with a default.

Source code in src/hassette/models/states/base.py
130
131
132
def extra(self, key: str, default: Any = None) -> Any:
    """Get a single extra field with a default."""
    return self.extras.get(key, default)

get_domain() -> str classmethod

Returns the domain string for this state class, extracted from the domain field annotation.

Source code in src/hassette/models/states/base.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
@classmethod
def get_domain(cls) -> str:
    """Returns the domain string for this state class, extracted from the domain field annotation."""

    fields = cls.model_fields
    domain_field = fields.get("domain")
    if not domain_field:
        raise NoDomainAnnotationError(cls)

    annotations = get_annotations(cls)
    annotation = annotations.get("domain")
    if annotation is None:
        raise NoDomainAnnotationError(cls)

    args = get_args(annotation)
    if not args:
        raise NoDomainAnnotationError(cls)

    domain = args[0]
    if not isinstance(domain, str):
        raise NoDomainAnnotationError(cls)

    return domain

StringBaseState

Bases: BaseState[str | None]

Base class for string states.

Source code in src/hassette/models/states/base.py
209
210
211
212
class StringBaseState(BaseState[str | None]):
    """Base class for string states."""

    value_type: ClassVar[type[Any] | tuple[type[Any], ...]] = (str, type(None))

DateTimeBaseState

Bases: BaseState[ZonedDateTime | PlainDateTime | Date | None]

Base class for datetime states.

Valid state values are ZonedDateTime, PlainDateTime, Date, or None.

Source code in src/hassette/models/states/base.py
215
216
217
218
219
220
221
class DateTimeBaseState(BaseState[ZonedDateTime | PlainDateTime | Date | None]):
    """Base class for datetime states.

    Valid state values are ZonedDateTime, PlainDateTime, Date, or None.
    """

    value_type: ClassVar[type[Any] | tuple[type[Any], ...]] = (ZonedDateTime, PlainDateTime, Date, type(None))

TimeBaseState

Bases: BaseState[Time | None]

Base class for Time states.

Valid state values are Time or None.

Source code in src/hassette/models/states/base.py
224
225
226
227
228
229
230
class TimeBaseState(BaseState[Time | None]):
    """Base class for Time states.

    Valid state values are Time or None.
    """

    value_type: ClassVar[type[Any] | tuple[type[Any], ...]] = (Time, type(None))

BoolBaseState

Bases: BaseState[bool | None]

Base class for boolean states.

Valid state values are True, False, or None.

Will convert string values "on" and "off" to boolean True and False.

Source code in src/hassette/models/states/base.py
233
234
235
236
237
238
239
240
241
class BoolBaseState(BaseState[bool | None]):
    """Base class for boolean states.

    Valid state values are True, False, or None.

    Will convert string values "on" and "off" to boolean True and False.
    """

    value_type: ClassVar[type[Any] | tuple[type[Any], ...]] = (bool, type(None))

NumericBaseState

Bases: BaseState[int | float | Decimal | None]

Base class for numeric states.

Will convert string values to float, int, or Decimal. Valid state values are int, float, Decimal, or None.

Source code in src/hassette/models/states/base.py
244
245
246
247
248
249
250
251
class NumericBaseState(BaseState[int | float | Decimal | None]):
    """Base class for numeric states.

    Will convert string values to float, int, or Decimal.
    Valid state values are int, float, Decimal, or None.
    """

    value_type: ClassVar[type[Any] | tuple[type[Any], ...]] = (int, float, Decimal, type(None))