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
207
208
209
210
211
212
213
214
215
216
217
218
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 time_since_last_change(self) -> TimeDelta | None:
        """Elapsed time since the state value last changed, or ``None`` if the timestamp is absent."""
        if self.last_changed is None:
            return None
        return date_utils.now() - self.last_changed

    @property
    def time_since_last_update(self) -> TimeDelta | None:
        """Elapsed time since the state or attributes last changed, or ``None`` if the timestamp is absent."""
        if self.last_updated is None:
            return None
        return date_utils.now() - self.last_updated

    @property
    def time_since_last_report(self) -> TimeDelta | None:
        """Elapsed time since the state was last reported, or ``None`` if the timestamp is absent."""
        if self.last_reported is None:
            return None
        return date_utils.now() - self.last_reported

    @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 date_utils.convert_utc_timestamp_to_tz(value)
        if isinstance(value, str):
            # need to use OffsetDateTime since the value is +00:00, not Z or a timezone
            return date_utils.convert_datetime_str_to_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

        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).

time_since_last_change: TimeDelta | None property

Elapsed time since the state value last changed, or None if the timestamp is absent.

time_since_last_update: TimeDelta | None property

Elapsed time since the state or attributes last changed, or None if the timestamp is absent.

time_since_last_report: TimeDelta | None property

Elapsed time since the state was last reported, or None if the timestamp is absent.

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
151
152
153
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
@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
221
222
223
224
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
227
228
229
230
231
232
233
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
236
237
238
239
240
241
242
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
245
246
247
248
249
250
251
252
253
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
256
257
258
259
260
261
262
263
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))