Merge branch 'dev' into jbouwh-mqtt-device-discovery

This commit is contained in:
J. Nick Koston
2024-05-26 01:23:15 -10:00
committed by GitHub
86 changed files with 2609 additions and 817 deletions

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"], "loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.9", "deebot-client==7.2.0"] "requirements": ["py-sucks==0.9.10", "deebot-client==7.2.0"]
} }

View File

@@ -292,7 +292,8 @@ class TimerManager:
timer.cancel() timer.cancel()
self.handlers[timer.device_id](TimerEventType.CANCELLED, timer) if timer.device_id in self.handlers:
self.handlers[timer.device_id](TimerEventType.CANCELLED, timer)
_LOGGER.debug( _LOGGER.debug(
"Timer cancelled: id=%s, name=%s, seconds_left=%s, device_id=%s", "Timer cancelled: id=%s, name=%s, seconds_left=%s, device_id=%s",
timer_id, timer_id,
@@ -320,7 +321,8 @@ class TimerManager:
name=f"Timer {timer_id}", name=f"Timer {timer_id}",
) )
self.handlers[timer.device_id](TimerEventType.UPDATED, timer) if timer.device_id in self.handlers:
self.handlers[timer.device_id](TimerEventType.UPDATED, timer)
if seconds > 0: if seconds > 0:
log_verb = "increased" log_verb = "increased"
@@ -357,7 +359,8 @@ class TimerManager:
task = self.timer_tasks.pop(timer_id) task = self.timer_tasks.pop(timer_id)
task.cancel() task.cancel()
self.handlers[timer.device_id](TimerEventType.UPDATED, timer) if timer.device_id in self.handlers:
self.handlers[timer.device_id](TimerEventType.UPDATED, timer)
_LOGGER.debug( _LOGGER.debug(
"Timer paused: id=%s, name=%s, seconds_left=%s, device_id=%s", "Timer paused: id=%s, name=%s, seconds_left=%s, device_id=%s",
timer_id, timer_id,
@@ -382,7 +385,8 @@ class TimerManager:
name=f"Timer {timer.id}", name=f"Timer {timer.id}",
) )
self.handlers[timer.device_id](TimerEventType.UPDATED, timer) if timer.device_id in self.handlers:
self.handlers[timer.device_id](TimerEventType.UPDATED, timer)
_LOGGER.debug( _LOGGER.debug(
"Timer unpaused: id=%s, name=%s, seconds_left=%s, device_id=%s", "Timer unpaused: id=%s, name=%s, seconds_left=%s, device_id=%s",
timer_id, timer_id,
@@ -397,7 +401,8 @@ class TimerManager:
timer.finish() timer.finish()
self.handlers[timer.device_id](TimerEventType.FINISHED, timer) if timer.device_id in self.handlers:
self.handlers[timer.device_id](TimerEventType.FINISHED, timer)
_LOGGER.debug( _LOGGER.debug(
"Timer finished: id=%s, name=%s, device_id=%s", "Timer finished: id=%s, name=%s, device_id=%s",
timer_id, timer_id,

View File

@@ -25,7 +25,7 @@ from homeassistant.const import (
STATE_ALARM_PENDING, STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED, STATE_ALARM_TRIGGERED,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HassJobType, HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@@ -220,6 +220,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )
@@ -309,13 +310,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
"""Publish via mqtt.""" """Publish via mqtt."""
variables = {"action": action, "code": code} variables = {"action": action, "code": code}
payload = self._command_template(None, variables=variables) payload = self._command_template(None, variables=variables)
await self.async_publish( await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
def _validate_code(self, code: str | None, state: str) -> bool: def _validate_code(self, code: str | None, state: str) -> bool:
"""Validate given code.""" """Validate given code."""

View File

@@ -26,7 +26,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.helpers.event as evt import homeassistant.helpers.event as evt
@@ -248,6 +248,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )

View File

@@ -14,13 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
from .const import ( from .const import CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_RETAIN
CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC,
CONF_ENCODING,
CONF_QOS,
CONF_RETAIN,
)
from .mixins import MqttEntity, async_setup_entity_entry_helper from .mixins import MqttEntity, async_setup_entity_entry_helper
from .models import MqttCommandTemplate from .models import MqttCommandTemplate
from .schemas import MQTT_ENTITY_COMMON_SCHEMA from .schemas import MQTT_ENTITY_COMMON_SCHEMA
@@ -91,10 +85,4 @@ class MqttButton(MqttEntity, ButtonEntity):
This method is a coroutine. This method is a coroutine.
""" """
payload = self._command_template(self._config[CONF_PAYLOAD_PRESS]) payload = self._command_template(self._config[CONF_PAYLOAD_PRESS])
await self.async_publish( await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)

View File

@@ -13,7 +13,7 @@ from homeassistant.components import camera
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -124,6 +124,7 @@ class MqttCamera(MqttEntity, Camera):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": None, "encoding": None,
"job_type": HassJobType.Callback,
} }
}, },
) )

View File

@@ -201,6 +201,7 @@ def async_subscribe_internal(
msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None], msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None],
qos: int = DEFAULT_QOS, qos: int = DEFAULT_QOS,
encoding: str | None = DEFAULT_ENCODING, encoding: str | None = DEFAULT_ENCODING,
job_type: HassJobType | None = None,
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Subscribe to an MQTT topic. """Subscribe to an MQTT topic.
@@ -228,7 +229,7 @@ def async_subscribe_internal(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_placeholders={"topic": topic}, translation_placeholders={"topic": topic},
) )
return client.async_subscribe(topic, msg_callback, qos, encoding) return client.async_subscribe(topic, msg_callback, qos, encoding, job_type)
@bind_hass @bind_hass
@@ -867,12 +868,14 @@ class MQTT:
msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None], msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None],
qos: int, qos: int,
encoding: str | None = None, encoding: str | None = None,
job_type: HassJobType | None = None,
) -> Callable[[], None]: ) -> Callable[[], None]:
"""Set up a subscription to a topic with the provided qos.""" """Set up a subscription to a topic with the provided qos."""
if not isinstance(topic, str): if not isinstance(topic, str):
raise HomeAssistantError("Topic needs to be a string!") raise HomeAssistantError("Topic needs to be a string!")
job_type = get_hassjob_callable_job_type(msg_callback) if job_type is None:
job_type = get_hassjob_callable_job_type(msg_callback)
if job_type is not HassJobType.Callback: if job_type is not HassJobType.Callback:
# Only wrap the callback with catch_log_exception # Only wrap the callback with catch_log_exception
# if it is not a simple callback since we catch # if it is not a simple callback since we catch

View File

@@ -43,7 +43,7 @@ from homeassistant.const import (
PRECISION_WHOLE, PRECISION_WHOLE,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
@@ -429,6 +429,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": qos, "qos": qos,
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
def render_template( def render_template(
@@ -515,13 +516,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
async def _publish(self, topic: str, payload: PublishPayloadType) -> None: async def _publish(self, topic: str, payload: PublishPayloadType) -> None:
if self._topic[topic] is not None: if self._topic[topic] is not None:
await self.async_publish( await self.async_publish_with_config(self._topic[topic], payload)
self._topic[topic],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
async def _set_climate_attribute( async def _set_climate_attribute(
self, self,

View File

@@ -28,7 +28,7 @@ from homeassistant.const import (
STATE_OPEN, STATE_OPEN,
STATE_OPENING, STATE_OPENING,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
@@ -478,6 +478,7 @@ class MqttCover(MqttEntity, CoverEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
if self._config.get(CONF_STATE_TOPIC): if self._config.get(CONF_STATE_TOPIC):
@@ -491,6 +492,7 @@ class MqttCover(MqttEntity, CoverEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
if self._config.get(CONF_TILT_STATUS_TOPIC) is not None: if self._config.get(CONF_TILT_STATUS_TOPIC) is not None:
@@ -504,6 +506,7 @@ class MqttCover(MqttEntity, CoverEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
self._sub_state = subscription.async_prepare_subscribe_topics( self._sub_state = subscription.async_prepare_subscribe_topics(
@@ -519,12 +522,8 @@ class MqttCover(MqttEntity, CoverEntity):
This method is a coroutine. This method is a coroutine.
""" """
await self.async_publish( await self.async_publish_with_config(
self._config[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_OPEN]
self._config[CONF_PAYLOAD_OPEN],
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
# Optimistically assume that cover has changed state. # Optimistically assume that cover has changed state.
@@ -538,12 +537,8 @@ class MqttCover(MqttEntity, CoverEntity):
This method is a coroutine. This method is a coroutine.
""" """
await self.async_publish( await self.async_publish_with_config(
self._config[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_CLOSE]
self._config[CONF_PAYLOAD_CLOSE],
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
# Optimistically assume that cover has changed state. # Optimistically assume that cover has changed state.
@@ -557,12 +552,8 @@ class MqttCover(MqttEntity, CoverEntity):
This method is a coroutine. This method is a coroutine.
""" """
await self.async_publish( await self.async_publish_with_config(
self._config[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_STOP]
self._config[CONF_PAYLOAD_STOP],
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
async def async_open_cover_tilt(self, **kwargs: Any) -> None: async def async_open_cover_tilt(self, **kwargs: Any) -> None:
@@ -577,12 +568,8 @@ class MqttCover(MqttEntity, CoverEntity):
"tilt_max": self._config.get(CONF_TILT_MAX), "tilt_max": self._config.get(CONF_TILT_MAX),
} }
tilt_payload = self._set_tilt_template(tilt_open_position, variables=variables) tilt_payload = self._set_tilt_template(tilt_open_position, variables=variables)
await self.async_publish( await self.async_publish_with_config(
self._config[CONF_TILT_COMMAND_TOPIC], self._config[CONF_TILT_COMMAND_TOPIC], tilt_payload
tilt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._tilt_optimistic: if self._tilt_optimistic:
self._attr_current_cover_tilt_position = self._tilt_open_percentage self._attr_current_cover_tilt_position = self._tilt_open_percentage
@@ -602,12 +589,8 @@ class MqttCover(MqttEntity, CoverEntity):
tilt_payload = self._set_tilt_template( tilt_payload = self._set_tilt_template(
tilt_closed_position, variables=variables tilt_closed_position, variables=variables
) )
await self.async_publish( await self.async_publish_with_config(
self._config[CONF_TILT_COMMAND_TOPIC], self._config[CONF_TILT_COMMAND_TOPIC], tilt_payload
tilt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._tilt_optimistic: if self._tilt_optimistic:
self._attr_current_cover_tilt_position = self._tilt_closed_percentage self._attr_current_cover_tilt_position = self._tilt_closed_percentage
@@ -630,13 +613,8 @@ class MqttCover(MqttEntity, CoverEntity):
"tilt_max": self._config.get(CONF_TILT_MAX), "tilt_max": self._config.get(CONF_TILT_MAX),
} }
tilt_rendered = self._set_tilt_template(tilt_ranged, variables=variables) tilt_rendered = self._set_tilt_template(tilt_ranged, variables=variables)
await self.async_publish_with_config(
await self.async_publish( self._config[CONF_TILT_COMMAND_TOPIC], tilt_rendered
self._config[CONF_TILT_COMMAND_TOPIC],
tilt_rendered,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._tilt_optimistic: if self._tilt_optimistic:
_LOGGER.debug("Set tilt value optimistic") _LOGGER.debug("Set tilt value optimistic")
@@ -660,13 +638,8 @@ class MqttCover(MqttEntity, CoverEntity):
position_rendered = self._set_position_template( position_rendered = self._set_position_template(
position_ranged, variables=variables position_ranged, variables=variables
) )
await self.async_publish_with_config(
await self.async_publish( self._config[CONF_SET_POSITION_TOPIC], position_rendered
self._config[CONF_SET_POSITION_TOPIC],
position_rendered,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
self._update_state( self._update_state(

View File

@@ -3,10 +3,8 @@
from __future__ import annotations from __future__ import annotations
from collections import deque from collections import deque
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
import datetime as dt import datetime as dt
from functools import wraps
import time import time
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@@ -21,34 +19,6 @@ from .models import DATA_MQTT, MessageCallbackType, PublishPayloadType
STORED_MESSAGES = 10 STORED_MESSAGES = 10
def log_messages(
hass: HomeAssistant, entity_id: str
) -> Callable[[MessageCallbackType], MessageCallbackType]:
"""Wrap an MQTT message callback to support message logging."""
debug_info_entities = hass.data[DATA_MQTT].debug_info_entities
def _log_message(msg: Any) -> None:
"""Log message."""
messages = debug_info_entities[entity_id]["subscriptions"][
msg.subscribed_topic
]["messages"]
if msg not in messages:
messages.append(msg)
def _decorator(msg_callback: MessageCallbackType) -> MessageCallbackType:
@wraps(msg_callback)
def wrapper(msg: Any) -> None:
"""Log message."""
_log_message(msg)
msg_callback(msg)
setattr(wrapper, "__entity_id", entity_id)
return wrapper
return _decorator
@dataclass @dataclass
class TimestampedPublishMessage: class TimestampedPublishMessage:
"""MQTT Message.""" """MQTT Message."""

View File

@@ -25,7 +25,7 @@ from homeassistant.const import (
STATE_HOME, STATE_HOME,
STATE_NOT_HOME, STATE_NOT_HOME,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@@ -155,6 +155,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
), ),
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"job_type": HassJobType.Callback,
} }
}, },
) )

View File

@@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.const import CONF_DEVICE, CONF_PLATFORM
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@@ -476,10 +476,14 @@ async def async_start( # noqa: C901
hass, MQTT_DISCOVERY_DONE.format(*discovery_hash), None hass, MQTT_DISCOVERY_DONE.format(*discovery_hash), None
) )
# async_subscribe will never suspend so there is no need to create a task
# here and its faster to await them in sequence
mqtt_data.discovery_unsubscribe = [ mqtt_data.discovery_unsubscribe = [
await mqtt.async_subscribe(hass, topic, async_discovery_message_received, 0) mqtt.async_subscribe_internal(
hass,
topic,
async_discovery_message_received,
0,
job_type=HassJobType.Callback,
)
for topic in ( for topic in (
f"{discovery_topic}/+/+/config", f"{discovery_topic}/+/+/config",
f"{discovery_topic}/+/+/+/config", f"{discovery_topic}/+/+/+/config",

View File

@@ -17,7 +17,7 @@ from homeassistant.components.event import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLATE from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLATE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@@ -200,6 +200,7 @@ class MqttEvent(MqttEntity, EventEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
self._sub_state = subscription.async_prepare_subscribe_topics( self._sub_state = subscription.async_prepare_subscribe_topics(

View File

@@ -27,7 +27,7 @@ from homeassistant.const import (
CONF_PAYLOAD_ON, CONF_PAYLOAD_ON,
CONF_STATE, CONF_STATE,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
@@ -45,7 +45,6 @@ from .const import (
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
CONF_STATE_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE,
PAYLOAD_NONE, PAYLOAD_NONE,
@@ -447,6 +446,7 @@ class MqttFan(MqttEntity, FanEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
return has_topic return has_topic
@@ -496,12 +496,8 @@ class MqttFan(MqttEntity, FanEntity):
This method is a coroutine. This method is a coroutine.
""" """
mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"]) mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"])
await self.async_publish( await self.async_publish_with_config(
self._topic[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], mqtt_payload
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if percentage: if percentage:
await self.async_set_percentage(percentage) await self.async_set_percentage(percentage)
@@ -517,12 +513,8 @@ class MqttFan(MqttEntity, FanEntity):
This method is a coroutine. This method is a coroutine.
""" """
mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"]) mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"])
await self.async_publish( await self.async_publish_with_config(
self._topic[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], mqtt_payload
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
self._attr_is_on = False self._attr_is_on = False
@@ -537,14 +529,9 @@ class MqttFan(MqttEntity, FanEntity):
percentage_to_ranged_value(self._speed_range, percentage) percentage_to_ranged_value(self._speed_range, percentage)
) )
mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload) mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload)
await self.async_publish( await self.async_publish_with_config(
self._topic[CONF_PERCENTAGE_COMMAND_TOPIC], self._config[CONF_PERCENTAGE_COMMAND_TOPIC], mqtt_payload
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic_percentage: if self._optimistic_percentage:
self._attr_percentage = percentage self._attr_percentage = percentage
self.async_write_ha_state() self.async_write_ha_state()
@@ -555,15 +542,9 @@ class MqttFan(MqttEntity, FanEntity):
This method is a coroutine. This method is a coroutine.
""" """
mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode) mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode)
await self.async_publish_with_config(
await self.async_publish( self._config[CONF_PRESET_MODE_COMMAND_TOPIC], mqtt_payload
self._topic[CONF_PRESET_MODE_COMMAND_TOPIC],
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic_preset_mode: if self._optimistic_preset_mode:
self._attr_preset_mode = preset_mode self._attr_preset_mode = preset_mode
self.async_write_ha_state() self.async_write_ha_state()
@@ -581,15 +562,9 @@ class MqttFan(MqttEntity, FanEntity):
mqtt_payload = self._command_templates[ATTR_OSCILLATING]( mqtt_payload = self._command_templates[ATTR_OSCILLATING](
self._payload["OSCILLATE_OFF_PAYLOAD"] self._payload["OSCILLATE_OFF_PAYLOAD"]
) )
await self.async_publish_with_config(
await self.async_publish( self._config[CONF_OSCILLATION_COMMAND_TOPIC], mqtt_payload
self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic_oscillation: if self._optimistic_oscillation:
self._attr_oscillating = oscillating self._attr_oscillating = oscillating
self.async_write_ha_state() self.async_write_ha_state()
@@ -600,15 +575,9 @@ class MqttFan(MqttEntity, FanEntity):
This method is a coroutine. This method is a coroutine.
""" """
mqtt_payload = self._command_templates[ATTR_DIRECTION](direction) mqtt_payload = self._command_templates[ATTR_DIRECTION](direction)
await self.async_publish_with_config(
await self.async_publish( self._config[CONF_DIRECTION_COMMAND_TOPIC], mqtt_payload
self._topic[CONF_DIRECTION_COMMAND_TOPIC],
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic_direction: if self._optimistic_direction:
self._attr_current_direction = direction self._attr_current_direction = direction
self.async_write_ha_state() self.async_write_ha_state()

View File

@@ -30,7 +30,7 @@ from homeassistant.const import (
CONF_PAYLOAD_ON, CONF_PAYLOAD_ON,
CONF_STATE, CONF_STATE,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
@@ -47,7 +47,6 @@ from .const import (
CONF_CURRENT_HUMIDITY_TOPIC, CONF_CURRENT_HUMIDITY_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
CONF_STATE_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE,
PAYLOAD_NONE, PAYLOAD_NONE,
@@ -293,6 +292,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": qos, "qos": qos,
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
@callback @callback
@@ -455,12 +455,8 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
This method is a coroutine. This method is a coroutine.
""" """
mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"]) mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"])
await self.async_publish( await self.async_publish_with_config(
self._topic[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], mqtt_payload
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
self._attr_is_on = True self._attr_is_on = True
@@ -472,12 +468,8 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
This method is a coroutine. This method is a coroutine.
""" """
mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"]) mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"])
await self.async_publish( await self.async_publish_with_config(
self._topic[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], mqtt_payload
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
self._attr_is_on = False self._attr_is_on = False
@@ -489,14 +481,9 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
This method is a coroutine. This method is a coroutine.
""" """
mqtt_payload = self._command_templates[ATTR_HUMIDITY](humidity) mqtt_payload = self._command_templates[ATTR_HUMIDITY](humidity)
await self.async_publish( await self.async_publish_with_config(
self._topic[CONF_TARGET_HUMIDITY_COMMAND_TOPIC], self._config[CONF_TARGET_HUMIDITY_COMMAND_TOPIC], mqtt_payload
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic_target_humidity: if self._optimistic_target_humidity:
self._attr_target_humidity = humidity self._attr_target_humidity = humidity
self.async_write_ha_state() self.async_write_ha_state()
@@ -511,15 +498,9 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
return return
mqtt_payload = self._command_templates[ATTR_MODE](mode) mqtt_payload = self._command_templates[ATTR_MODE](mode)
await self.async_publish_with_config(
await self.async_publish( self._config[CONF_MODE_COMMAND_TOPIC], mqtt_payload
self._topic[CONF_MODE_COMMAND_TOPIC],
mqtt_payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic_mode: if self._optimistic_mode:
self._attr_mode = mode self._attr_mode = mode
self.async_write_ha_state() self.async_write_ha_state()

View File

@@ -16,7 +16,7 @@ from homeassistant.components import image
from homeassistant.components.image import DEFAULT_CONTENT_TYPE, ImageEntity from homeassistant.components.image import DEFAULT_CONTENT_TYPE, ImageEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.httpx_client import get_async_client
@@ -202,6 +202,7 @@ class MqttImage(MqttEntity, ImageEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": encoding, "encoding": encoding,
"job_type": HassJobType.Callback,
} }
return has_topic return has_topic

View File

@@ -17,7 +17,7 @@ from homeassistant.components.lawn_mower import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
@@ -192,6 +192,7 @@ class MqttLawnMower(MqttEntity, LawnMowerEntity, RestoreEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )
@@ -212,14 +213,7 @@ class MqttLawnMower(MqttEntity, LawnMowerEntity, RestoreEntity):
if self._attr_assumed_state: if self._attr_assumed_state:
self._attr_activity = activity self._attr_activity = activity
self.async_write_ha_state() self.async_write_ha_state()
await self.async_publish_with_config(self._command_topics[option], payload)
await self.async_publish(
self._command_topics[option],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
async def async_start_mowing(self) -> None: async def async_start_mowing(self) -> None:
"""Start or resume mowing.""" """Start or resume mowing."""

View File

@@ -37,7 +37,7 @@ from homeassistant.const import (
CONF_PAYLOAD_ON, CONF_PAYLOAD_ON,
STATE_ON, STATE_ON,
) )
from homeassistant.core import callback from homeassistant.core import HassJobType, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@@ -49,7 +49,6 @@ from ..const import (
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
CONF_STATE_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE,
PAYLOAD_NONE, PAYLOAD_NONE,
@@ -580,6 +579,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
add_topic(CONF_STATE_TOPIC, self._state_received, {"_attr_is_on"}) add_topic(CONF_STATE_TOPIC, self._state_received, {"_attr_is_on"})
@@ -664,13 +664,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
async def publish(topic: str, payload: PublishPayloadType) -> None: async def publish(topic: str, payload: PublishPayloadType) -> None:
"""Publish an MQTT message.""" """Publish an MQTT message."""
await self.async_publish( await self.async_publish_with_config(str(self._topic[topic]), payload)
str(self._topic[topic]),
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
def scale_rgbx( def scale_rgbx(
color: tuple[int, ...], color: tuple[int, ...],
@@ -875,12 +869,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
This method is a coroutine. This method is a coroutine.
""" """
await self.async_publish( await self.async_publish_with_config(
str(self._topic[CONF_COMMAND_TOPIC]), str(self._topic[CONF_COMMAND_TOPIC]), self._payload["off"]
self._payload["off"],
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:

View File

@@ -47,7 +47,7 @@ from homeassistant.const import (
CONF_XY, CONF_XY,
STATE_ON, STATE_ON,
) )
from homeassistant.core import async_get_hass, callback from homeassistant.core import HassJobType, async_get_hass, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.json import json_dumps from homeassistant.helpers.json import json_dumps
@@ -522,6 +522,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )
@@ -737,12 +738,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
self._attr_brightness = kwargs[ATTR_WHITE] self._attr_brightness = kwargs[ATTR_WHITE]
should_update = True should_update = True
await self.async_publish( await self.async_publish_with_config(
str(self._topic[CONF_COMMAND_TOPIC]), str(self._topic[CONF_COMMAND_TOPIC]), json_dumps(message)
json_dumps(message),
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
@@ -762,12 +759,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
self._set_flash_and_transition(message, **kwargs) self._set_flash_and_transition(message, **kwargs)
await self.async_publish( await self.async_publish_with_config(
str(self._topic[CONF_COMMAND_TOPIC]), str(self._topic[CONF_COMMAND_TOPIC]), json_dumps(message)
json_dumps(message),
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:

View File

@@ -29,7 +29,7 @@ from homeassistant.const import (
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
) )
from homeassistant.core import callback from homeassistant.core import HassJobType, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.typing import ConfigType, TemplateVarsType
@@ -41,7 +41,6 @@ from ..const import (
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
PAYLOAD_NONE, PAYLOAD_NONE,
) )
@@ -282,6 +281,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )
@@ -364,12 +364,9 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
values["transition"] = kwargs[ATTR_TRANSITION] values["transition"] = kwargs[ATTR_TRANSITION]
await self.async_publish( await self.async_publish_with_config(
str(self._topics[CONF_COMMAND_TOPIC]), str(self._topics[CONF_COMMAND_TOPIC]),
self._command_templates[CONF_COMMAND_ON_TEMPLATE](None, values), self._command_templates[CONF_COMMAND_ON_TEMPLATE](None, values),
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
@@ -387,12 +384,9 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
values["transition"] = kwargs[ATTR_TRANSITION] values["transition"] = kwargs[ATTR_TRANSITION]
await self.async_publish( await self.async_publish_with_config(
str(self._topics[CONF_COMMAND_TOPIC]), str(self._topics[CONF_COMMAND_TOPIC]),
self._command_templates[CONF_COMMAND_OFF_TEMPLATE](None, values), self._command_templates[CONF_COMMAND_OFF_TEMPLATE](None, values),
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:

View File

@@ -19,7 +19,7 @@ from homeassistant.const import (
CONF_OPTIMISTIC, CONF_OPTIMISTIC,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.typing import ConfigType, TemplateVarsType
@@ -32,7 +32,6 @@ from .const import (
CONF_ENCODING, CONF_ENCODING,
CONF_PAYLOAD_RESET, CONF_PAYLOAD_RESET,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_OPEN, CONF_STATE_OPEN,
CONF_STATE_OPENING, CONF_STATE_OPENING,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
@@ -232,6 +231,7 @@ class MqttLock(MqttEntity, LockEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
CONF_QOS: qos, CONF_QOS: qos,
CONF_ENCODING: encoding, CONF_ENCODING: encoding,
"job_type": HassJobType.Callback,
} }
} }
@@ -254,13 +254,7 @@ class MqttLock(MqttEntity, LockEntity):
ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None
} }
payload = self._command_template(self._config[CONF_PAYLOAD_LOCK], tpl_vars) payload = self._command_template(self._config[CONF_PAYLOAD_LOCK], tpl_vars)
await self.async_publish( await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
if self._optimistic: if self._optimistic:
# Optimistically assume that the lock has changed state. # Optimistically assume that the lock has changed state.
self._attr_is_locked = True self._attr_is_locked = True
@@ -275,13 +269,7 @@ class MqttLock(MqttEntity, LockEntity):
ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None
} }
payload = self._command_template(self._config[CONF_PAYLOAD_UNLOCK], tpl_vars) payload = self._command_template(self._config[CONF_PAYLOAD_UNLOCK], tpl_vars)
await self.async_publish( await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
if self._optimistic: if self._optimistic:
# Optimistically assume that the lock has changed state. # Optimistically assume that the lock has changed state.
self._attr_is_locked = False self._attr_is_locked = False
@@ -296,13 +284,7 @@ class MqttLock(MqttEntity, LockEntity):
ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None
} }
payload = self._command_template(self._config[CONF_PAYLOAD_OPEN], tpl_vars) payload = self._command_template(self._config[CONF_PAYLOAD_OPEN], tpl_vars)
await self.async_publish( await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
if self._optimistic: if self._optimistic:
# Optimistically assume that the lock unlocks when opened. # Optimistically assume that the lock unlocks when opened.
self._attr_is_open = True self._attr_is_open = True

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
import functools import functools
from functools import partial, wraps from functools import partial
import logging import logging
from typing import TYPE_CHECKING, Any, Protocol, cast, final from typing import TYPE_CHECKING, Any, Protocol, cast, final
@@ -30,7 +30,7 @@ from homeassistant.const import (
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
) )
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HassJobType, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import ( from homeassistant.helpers.device_registry import (
DeviceEntry, DeviceEntry,
@@ -83,6 +83,7 @@ from .const import (
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_SCHEMA, CONF_SCHEMA,
CONF_SERIAL_NUMBER, CONF_SERIAL_NUMBER,
CONF_SUGGESTED_AREA, CONF_SUGGESTED_AREA,
@@ -359,45 +360,6 @@ def init_entity_id_from_config(
) )
def write_state_on_attr_change(
entity: Entity, attributes: set[str]
) -> Callable[[MessageCallbackType], MessageCallbackType]:
"""Wrap an MQTT message callback to track state attribute changes."""
def _attrs_have_changed(tracked_attrs: dict[str, Any]) -> bool:
"""Return True if attributes on entity changed or if update is forced."""
if not (write_state := (getattr(entity, "_attr_force_update", False))):
for attribute, last_value in tracked_attrs.items():
if getattr(entity, attribute, UNDEFINED) != last_value:
write_state = True
break
return write_state
def _decorator(msg_callback: MessageCallbackType) -> MessageCallbackType:
@wraps(msg_callback)
def wrapper(msg: ReceiveMessage) -> None:
"""Track attributes for write state requests."""
tracked_attrs: dict[str, Any] = {
attribute: getattr(entity, attribute, UNDEFINED)
for attribute in attributes
}
try:
msg_callback(msg)
except MqttValueTemplateException as exc:
_LOGGER.warning(exc)
return
if not _attrs_have_changed(tracked_attrs):
return
mqtt_data = entity.hass.data[DATA_MQTT]
mqtt_data.state_write_requests.write_state_request(entity)
return wrapper
return _decorator
class MqttAttributesMixin(Entity): class MqttAttributesMixin(Entity):
"""Mixin used for platforms that support JSON attributes.""" """Mixin used for platforms that support JSON attributes."""
@@ -426,9 +388,10 @@ class MqttAttributesMixin(Entity):
def _attributes_prepare_subscribe_topics(self) -> None: def _attributes_prepare_subscribe_topics(self) -> None:
"""(Re)Subscribe to topics.""" """(Re)Subscribe to topics."""
self._attr_tpl = MqttValueTemplate( if template := self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE):
self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE), entity=self self._attr_tpl = MqttValueTemplate(
).async_render_with_possible_json_value template, entity=self
).async_render_with_possible_json_value
self._attributes_sub_state = async_prepare_subscribe_topics( self._attributes_sub_state = async_prepare_subscribe_topics(
self.hass, self.hass,
self._attributes_sub_state, self._attributes_sub_state,
@@ -443,6 +406,7 @@ class MqttAttributesMixin(Entity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._attributes_config.get(CONF_QOS), "qos": self._attributes_config.get(CONF_QOS),
"encoding": self._attributes_config[CONF_ENCODING] or None, "encoding": self._attributes_config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )
@@ -461,9 +425,9 @@ class MqttAttributesMixin(Entity):
@callback @callback
def _attributes_message_received(self, msg: ReceiveMessage) -> None: def _attributes_message_received(self, msg: ReceiveMessage) -> None:
"""Update extra state attributes.""" """Update extra state attributes."""
if TYPE_CHECKING: payload = (
assert self._attr_tpl is not None self._attr_tpl(msg.payload) if self._attr_tpl is not None else msg.payload
payload = self._attr_tpl(msg.payload) )
try: try:
json_dict = json_loads(payload) if isinstance(payload, str) else None json_dict = json_loads(payload) if isinstance(payload, str) else None
except ValueError: except ValueError:
@@ -557,6 +521,7 @@ class MqttAvailabilityMixin(Entity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._avail_config[CONF_QOS], "qos": self._avail_config[CONF_QOS],
"encoding": self._avail_config[CONF_ENCODING] or None, "encoding": self._avail_config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
for topic in self._avail_topics for topic in self._avail_topics
} }
@@ -1192,6 +1157,18 @@ class MqttEntity(
encoding, encoding,
) )
async def async_publish_with_config(
self, topic: str, payload: PublishPayloadType
) -> None:
"""Publish payload to a topic using config."""
await self.async_publish(
topic,
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def config_schema() -> vol.Schema: def config_schema() -> vol.Schema:

View File

@@ -14,13 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
from .const import ( from .const import CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_RETAIN
CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC,
CONF_ENCODING,
CONF_QOS,
CONF_RETAIN,
)
from .mixins import MqttEntity, async_setup_entity_entry_helper from .mixins import MqttEntity, async_setup_entity_entry_helper
from .models import MqttCommandTemplate from .models import MqttCommandTemplate
from .schemas import MQTT_ENTITY_COMMON_SCHEMA from .schemas import MQTT_ENTITY_COMMON_SCHEMA
@@ -83,10 +77,4 @@ class MqttNotify(MqttEntity, NotifyEntity):
async def async_send_message(self, message: str, title: str | None = None) -> None: async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message.""" """Send a message."""
payload = self._command_template(message) payload = self._command_template(message)
await self.async_publish( await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)

View File

@@ -26,7 +26,7 @@ from homeassistant.const import (
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@@ -39,7 +39,6 @@ from .const import (
CONF_ENCODING, CONF_ENCODING,
CONF_PAYLOAD_RESET, CONF_PAYLOAD_RESET,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
) )
from .mixins import MqttEntity, async_setup_entity_entry_helper from .mixins import MqttEntity, async_setup_entity_entry_helper
@@ -214,6 +213,7 @@ class MqttNumber(MqttEntity, RestoreNumber):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )
@@ -238,11 +238,4 @@ class MqttNumber(MqttEntity, RestoreNumber):
if self._attr_assumed_state: if self._attr_assumed_state:
self._attr_native_value = current_number self._attr_native_value = current_number
self.async_write_ha_state() self.async_write_ha_state()
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
await self.async_publish(
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .config import MQTT_BASE_SCHEMA from .config import MQTT_BASE_SCHEMA
from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from .const import CONF_COMMAND_TOPIC, CONF_RETAIN
from .mixins import MqttEntity, async_setup_entity_entry_helper from .mixins import MqttEntity, async_setup_entity_entry_helper
from .schemas import MQTT_ENTITY_COMMON_SCHEMA from .schemas import MQTT_ENTITY_COMMON_SCHEMA
from .util import valid_publish_topic from .util import valid_publish_topic
@@ -83,10 +83,6 @@ class MqttScene(
This method is a coroutine. This method is a coroutine.
""" """
await self.async_publish( await self.async_publish_with_config(
self._config[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON]
self._config[CONF_PAYLOAD_ON],
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )

View File

@@ -12,7 +12,7 @@ from homeassistant.components import select
from homeassistant.components.select import SelectEntity from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
@@ -25,7 +25,6 @@ from .const import (
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
) )
from .mixins import MqttEntity, async_setup_entity_entry_helper from .mixins import MqttEntity, async_setup_entity_entry_helper
@@ -154,6 +153,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )
@@ -173,11 +173,4 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
if self._attr_assumed_state: if self._attr_assumed_state:
self._attr_current_option = option self._attr_current_option = option
self.async_write_ha_state() self.async_write_ha_state()
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
await self.async_publish(
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)

View File

@@ -31,7 +31,13 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback from homeassistant.core import (
CALLBACK_TYPE,
HassJobType,
HomeAssistant,
State,
callback,
)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
@@ -297,6 +303,7 @@ class MqttSensor(MqttEntity, RestoreSensor):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
self._sub_state = subscription.async_prepare_subscribe_topics( self._sub_state = subscription.async_prepare_subscribe_topics(

View File

@@ -28,7 +28,7 @@ from homeassistant.const import (
CONF_PAYLOAD_OFF, CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON, CONF_PAYLOAD_ON,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import json_dumps from homeassistant.helpers.json import json_dumps
@@ -43,7 +43,6 @@ from .const import (
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
CONF_STATE_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE,
PAYLOAD_EMPTY_JSON, PAYLOAD_EMPTY_JSON,
@@ -282,6 +281,7 @@ class MqttSiren(MqttEntity, SirenEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )
@@ -318,13 +318,7 @@ class MqttSiren(MqttEntity, SirenEntity):
else: else:
payload = json_dumps(template_variables) payload = json_dumps(template_variables)
if payload and str(payload) != PAYLOAD_NONE: if payload and str(payload) != PAYLOAD_NONE:
await self.async_publish( await self.async_publish_with_config(self._config[topic], payload)
self._config[topic],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the siren on. """Turn the siren on.

View File

@@ -7,7 +7,7 @@ from dataclasses import dataclass
from functools import partial from functools import partial
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
from . import debug_info from . import debug_info
from .client import async_subscribe_internal from .client import async_subscribe_internal
@@ -27,6 +27,7 @@ class EntitySubscription:
qos: int = 0 qos: int = 0
encoding: str = "utf-8" encoding: str = "utf-8"
entity_id: str | None = None entity_id: str | None = None
job_type: HassJobType | None = None
def resubscribe_if_necessary( def resubscribe_if_necessary(
self, hass: HomeAssistant, other: EntitySubscription | None self, hass: HomeAssistant, other: EntitySubscription | None
@@ -62,7 +63,12 @@ class EntitySubscription:
if not self.should_subscribe or not self.topic: if not self.should_subscribe or not self.topic:
return return
self.unsubscribe_callback = async_subscribe_internal( self.unsubscribe_callback = async_subscribe_internal(
self.hass, self.topic, self.message_callback, self.qos, self.encoding self.hass,
self.topic,
self.message_callback,
self.qos,
self.encoding,
self.job_type,
) )
def _should_resubscribe(self, other: EntitySubscription | None) -> bool: def _should_resubscribe(self, other: EntitySubscription | None) -> bool:
@@ -112,6 +118,7 @@ def async_prepare_subscribe_topics(
hass=hass, hass=hass,
should_subscribe=None, should_subscribe=None,
entity_id=value.get("entity_id", None), entity_id=value.get("entity_id", None),
job_type=value.get("job_type", None),
) )
# Get the current subscription state # Get the current subscription state
current = current_subscriptions.pop(key, None) current = current_subscriptions.pop(key, None)

View File

@@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
STATE_ON, STATE_ON,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
@@ -33,7 +33,6 @@ from .const import (
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
PAYLOAD_NONE, PAYLOAD_NONE,
) )
@@ -145,6 +144,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
}, },
) )
@@ -161,12 +161,8 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity):
This method is a coroutine. This method is a coroutine.
""" """
await self.async_publish( await self.async_publish_with_config(
self._config[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON]
self._config[CONF_PAYLOAD_ON],
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
# Optimistically assume that switch has changed state. # Optimistically assume that switch has changed state.
@@ -178,12 +174,8 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity):
This method is a coroutine. This method is a coroutine.
""" """
await self.async_publish( await self.async_publish_with_config(
self._config[CONF_COMMAND_TOPIC], self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_OFF]
self._config[CONF_PAYLOAD_OFF],
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
# Optimistically assume that switch has changed state. # Optimistically assume that switch has changed state.

View File

@@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant.components import tag from homeassistant.components import tag
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_VALUE_TEMPLATE from homeassistant.const import CONF_DEVICE, CONF_VALUE_TEMPLATE
from homeassistant.core import HomeAssistant from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -142,28 +142,32 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdateMixin):
update_device(self.hass, self._config_entry, config) update_device(self.hass, self._config_entry, config)
await self.subscribe_topics() await self.subscribe_topics()
@callback
def _async_tag_scanned(self, msg: ReceiveMessage) -> None:
"""Handle new tag scanned."""
try:
tag_id = str(self._value_template(msg.payload, "")).strip()
except MqttValueTemplateException as exc:
_LOGGER.warning(exc)
return
if not tag_id: # No output from template, ignore
return
self.hass.async_create_task(
tag.async_scan_tag(self.hass, tag_id, self.device_id)
)
async def subscribe_topics(self) -> None: async def subscribe_topics(self) -> None:
"""Subscribe to MQTT topics.""" """Subscribe to MQTT topics."""
async def tag_scanned(msg: ReceiveMessage) -> None:
try:
tag_id = str(self._value_template(msg.payload, "")).strip()
except MqttValueTemplateException as exc:
_LOGGER.warning(exc)
return
if not tag_id: # No output from template, ignore
return
await tag.async_scan_tag(self.hass, tag_id, self.device_id)
self._sub_state = subscription.async_prepare_subscribe_topics( self._sub_state = subscription.async_prepare_subscribe_topics(
self.hass, self.hass,
self._sub_state, self._sub_state,
{ {
"state_topic": { "state_topic": {
"topic": self._config[CONF_TOPIC], "topic": self._config[CONF_TOPIC],
"msg_callback": tag_scanned, "msg_callback": self._async_tag_scanned,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"job_type": HassJobType.Callback,
} }
}, },
) )

View File

@@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
MAX_LENGTH_STATE_STATE, MAX_LENGTH_STATE_STATE,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@@ -32,7 +32,6 @@ from .const import (
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
) )
from .mixins import MqttEntity, async_setup_entity_entry_helper from .mixins import MqttEntity, async_setup_entity_entry_helper
@@ -183,6 +182,7 @@ class MqttTextEntity(MqttEntity, TextEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
add_subscription( add_subscription(
@@ -203,14 +203,7 @@ class MqttTextEntity(MqttEntity, TextEntity):
async def async_set_value(self, value: str) -> None: async def async_set_value(self, value: str) -> None:
"""Change the text.""" """Change the text."""
payload = self._command_template(value) payload = self._command_template(value)
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
await self.async_publish(
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
if self._optimistic: if self._optimistic:
self._attr_native_value = value self._attr_native_value = value
self.async_write_ha_state() self.async_write_ha_state()

View File

@@ -10,7 +10,13 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.core import (
CALLBACK_TYPE,
HassJob,
HassJobType,
HomeAssistant,
callback,
)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger import TriggerActionType, TriggerData, TriggerInfo from homeassistant.helpers.trigger import TriggerActionType, TriggerData, TriggerInfo
@@ -99,6 +105,11 @@ async def async_attach_trigger(
"Attaching MQTT trigger for topic: '%s', payload: '%s'", topic, wanted_payload "Attaching MQTT trigger for topic: '%s', payload: '%s'", topic, wanted_payload
) )
return await mqtt.async_subscribe( return mqtt.async_subscribe_internal(
hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos hass,
topic,
mqtt_automation_listener,
encoding=encoding,
qos=qos,
job_type=HassJobType.Callback,
) )

View File

@@ -16,7 +16,7 @@ from homeassistant.components.update import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLATE from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLATE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
@@ -229,6 +229,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
add_subscription( add_subscription(
@@ -264,14 +265,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
) -> None: ) -> None:
"""Update the current value.""" """Update the current value."""
payload = self._config[CONF_PAYLOAD_INSTALL] payload = self._config[CONF_PAYLOAD_INSTALL]
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
await self.async_publish(
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
@property @property
def supported_features(self) -> UpdateEntityFeature: def supported_features(self) -> UpdateEntityFeature:

View File

@@ -31,7 +31,7 @@ from homeassistant.const import (
STATE_IDLE, STATE_IDLE,
STATE_PAUSED, STATE_PAUSED,
) )
from homeassistant.core import HomeAssistant, async_get_hass, callback from homeassistant.core import HassJobType, HomeAssistant, async_get_hass, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
@@ -346,6 +346,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
self._sub_state = subscription.async_prepare_subscribe_topics( self._sub_state = subscription.async_prepare_subscribe_topics(
self.hass, self._sub_state, topics self.hass, self._sub_state, topics
@@ -359,13 +360,8 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
"""Publish a command.""" """Publish a command."""
if self._command_topic is None: if self._command_topic is None:
return return
await self.async_publish_with_config(
await self.async_publish( self._command_topic, self._payloads[_FEATURE_PAYLOADS[feature]]
self._command_topic,
self._payloads[_FEATURE_PAYLOADS[feature]],
qos=self._config[CONF_QOS],
retain=self._config[CONF_RETAIN],
encoding=self._config[CONF_ENCODING],
) )
self.async_write_ha_state() self.async_write_ha_state()
@@ -401,13 +397,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
or (fan_speed not in self.fan_speed_list) or (fan_speed not in self.fan_speed_list)
): ):
return return
await self.async_publish( await self.async_publish_with_config(self._set_fan_speed_topic, fan_speed)
self._set_fan_speed_topic,
fan_speed,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
async def async_send_command( async def async_send_command(
self, self,
@@ -427,10 +417,4 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
payload = json_dumps(message) payload = json_dumps(message)
else: else:
payload = command payload = command
await self.async_publish( await self.async_publish_with_config(self._send_command_topic, payload)
self._send_command_topic,
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)

View File

@@ -26,7 +26,7 @@ from homeassistant.const import (
STATE_OPEN, STATE_OPEN,
STATE_OPENING, STATE_OPENING,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HassJobType, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@@ -357,6 +357,7 @@ class MqttValve(MqttEntity, ValveEntity):
"entity_id": self.entity_id, "entity_id": self.entity_id,
"qos": self._config[CONF_QOS], "qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None, "encoding": self._config[CONF_ENCODING] or None,
"job_type": HassJobType.Callback,
} }
self._sub_state = subscription.async_prepare_subscribe_topics( self._sub_state = subscription.async_prepare_subscribe_topics(
@@ -375,13 +376,7 @@ class MqttValve(MqttEntity, ValveEntity):
payload = self._command_template( payload = self._command_template(
self._config.get(CONF_PAYLOAD_OPEN, DEFAULT_PAYLOAD_OPEN) self._config.get(CONF_PAYLOAD_OPEN, DEFAULT_PAYLOAD_OPEN)
) )
await self.async_publish( await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
if self._optimistic: if self._optimistic:
# Optimistically assume that valve has changed state. # Optimistically assume that valve has changed state.
self._update_state(STATE_OPEN) self._update_state(STATE_OPEN)
@@ -395,13 +390,7 @@ class MqttValve(MqttEntity, ValveEntity):
payload = self._command_template( payload = self._command_template(
self._config.get(CONF_PAYLOAD_CLOSE, DEFAULT_PAYLOAD_CLOSE) self._config.get(CONF_PAYLOAD_CLOSE, DEFAULT_PAYLOAD_CLOSE)
) )
await self.async_publish( await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
if self._optimistic: if self._optimistic:
# Optimistically assume that valve has changed state. # Optimistically assume that valve has changed state.
self._update_state(STATE_CLOSED) self._update_state(STATE_CLOSED)
@@ -413,13 +402,7 @@ class MqttValve(MqttEntity, ValveEntity):
This method is a coroutine. This method is a coroutine.
""" """
payload = self._command_template(self._config[CONF_PAYLOAD_STOP]) payload = self._command_template(self._config[CONF_PAYLOAD_STOP])
await self.async_publish( await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
self._config[CONF_COMMAND_TOPIC],
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
)
async def async_set_valve_position(self, position: int) -> None: async def async_set_valve_position(self, position: int) -> None:
"""Move the valve to a specific position.""" """Move the valve to a specific position."""
@@ -433,13 +416,8 @@ class MqttValve(MqttEntity, ValveEntity):
"position_closed": self._config[CONF_POSITION_CLOSED], "position_closed": self._config[CONF_POSITION_CLOSED],
} }
rendered_position = self._command_template(scaled_position, variables=variables) rendered_position = self._command_template(scaled_position, variables=variables)
await self.async_publish_with_config(
await self.async_publish( self._config[CONF_COMMAND_TOPIC], rendered_position
self._config[CONF_COMMAND_TOPIC],
rendered_position,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
) )
if self._optimistic: if self._optimistic:
self._update_state( self._update_state(

View File

@@ -609,6 +609,15 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity):
) )
coro = self._async_run(variables, context) coro = self._async_run(variables, context)
if wait: if wait:
# If we are executing in parallel, we need to copy the script stack so
# that if this script is called in parallel, it will not be seen in the
# stack of the other parallel calls and hit the disallowed recursion
# check as each parallel call would otherwise be appending to the same
# stack. We do not wipe the stack in this case because we still want to
# be able to detect if there is a disallowed recursion.
if script_stack := script_stack_cv.get():
script_stack_cv.set(script_stack.copy())
script_result = await coro script_result = await coro
return script_result.service_response if script_result else None return script_result.service_response if script_result else None

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/subaru", "documentation": "https://www.home-assistant.io/integrations/subaru",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["stdiomask", "subarulink"], "loggers": ["stdiomask", "subarulink"],
"requirements": ["subarulink==0.7.9"] "requirements": ["subarulink==0.7.11"]
} }

View File

@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any, cast from typing import Any
import subarulink.const as sc import subarulink.const as sc
@@ -23,11 +23,7 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator, DataUpdateCoordinator,
) )
from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter
from homeassistant.util.unit_system import ( from homeassistant.util.unit_system import METRIC_SYSTEM
LENGTH_UNITS,
PRESSURE_UNITS,
US_CUSTOMARY_SYSTEM,
)
from . import get_device_info from . import get_device_info
from .const import ( from .const import (
@@ -58,7 +54,7 @@ SAFETY_SENSORS = [
key=sc.ODOMETER, key=sc.ODOMETER,
translation_key="odometer", translation_key="odometer",
device_class=SensorDeviceClass.DISTANCE, device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS, native_unit_of_measurement=UnitOfLength.MILES,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
] ]
@@ -68,42 +64,42 @@ API_GEN_2_SENSORS = [
SensorEntityDescription( SensorEntityDescription(
key=sc.AVG_FUEL_CONSUMPTION, key=sc.AVG_FUEL_CONSUMPTION,
translation_key="average_fuel_consumption", translation_key="average_fuel_consumption",
native_unit_of_measurement=FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS, native_unit_of_measurement=FUEL_CONSUMPTION_MILES_PER_GALLON,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=sc.DIST_TO_EMPTY, key=sc.DIST_TO_EMPTY,
translation_key="range", translation_key="range",
device_class=SensorDeviceClass.DISTANCE, device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS, native_unit_of_measurement=UnitOfLength.MILES,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=sc.TIRE_PRESSURE_FL, key=sc.TIRE_PRESSURE_FL,
translation_key="tire_pressure_front_left", translation_key="tire_pressure_front_left",
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.HPA, native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=sc.TIRE_PRESSURE_FR, key=sc.TIRE_PRESSURE_FR,
translation_key="tire_pressure_front_right", translation_key="tire_pressure_front_right",
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.HPA, native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=sc.TIRE_PRESSURE_RL, key=sc.TIRE_PRESSURE_RL,
translation_key="tire_pressure_rear_left", translation_key="tire_pressure_rear_left",
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.HPA, native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=sc.TIRE_PRESSURE_RR, key=sc.TIRE_PRESSURE_RR,
translation_key="tire_pressure_rear_right", translation_key="tire_pressure_rear_right",
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.HPA, native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
] ]
@@ -207,30 +203,13 @@ class SubaruSensor(
@property @property
def native_value(self) -> int | float | None: def native_value(self) -> int | float | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
vehicle_data = self.coordinator.data[self.vin] current_value = self.coordinator.data[self.vin][VEHICLE_STATUS].get(
current_value = vehicle_data[VEHICLE_STATUS].get(self.entity_description.key) self.entity_description.key
unit = self.entity_description.native_unit_of_measurement )
unit_system = self.hass.config.units
if current_value is None:
return None
if unit in LENGTH_UNITS:
return round(unit_system.length(current_value, cast(str, unit)), 1)
if unit in PRESSURE_UNITS and unit_system == US_CUSTOMARY_SYSTEM:
return round(
unit_system.pressure(current_value, cast(str, unit)),
1,
)
if ( if (
unit self.entity_description.key == sc.AVG_FUEL_CONSUMPTION
in [ and self.hass.config.units == METRIC_SYSTEM
FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
FUEL_CONSUMPTION_MILES_PER_GALLON,
]
and unit_system == US_CUSTOMARY_SYSTEM
): ):
return round((100.0 * L_PER_GAL) / (KM_PER_MI * current_value), 1) return round((100.0 * L_PER_GAL) / (KM_PER_MI * current_value), 1)
@@ -239,23 +218,12 @@ class SubaruSensor(
@property @property
def native_unit_of_measurement(self) -> str | None: def native_unit_of_measurement(self) -> str | None:
"""Return the unit_of_measurement of the device.""" """Return the unit_of_measurement of the device."""
unit = self.entity_description.native_unit_of_measurement if (
self.entity_description.key == sc.AVG_FUEL_CONSUMPTION
if unit in LENGTH_UNITS: and self.hass.config.units == METRIC_SYSTEM
return self.hass.config.units.length_unit ):
return FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS
if unit in PRESSURE_UNITS: return self.entity_description.native_unit_of_measurement
if self.hass.config.units == US_CUSTOMARY_SYSTEM:
return self.hass.config.units.pressure_unit
if unit in [
FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
FUEL_CONSUMPTION_MILES_PER_GALLON,
]:
if self.hass.config.units == US_CUSTOMARY_SYSTEM:
return FUEL_CONSUMPTION_MILES_PER_GALLON
return unit
@property @property
def available(self) -> bool: def available(self) -> bool:

View File

@@ -2,11 +2,13 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, cast
from aioswitcher.api import ( from aioswitcher.api import (
DeviceState, DeviceState,
SwitcherApi,
SwitcherBaseResponse, SwitcherBaseResponse,
SwitcherType2Api, SwitcherType2Api,
ThermostatSwing, ThermostatSwing,
@@ -34,7 +36,10 @@ from .utils import get_breeze_remote_manager
class SwitcherThermostatButtonEntityDescription(ButtonEntityDescription): class SwitcherThermostatButtonEntityDescription(ButtonEntityDescription):
"""Class to describe a Switcher Thermostat Button entity.""" """Class to describe a Switcher Thermostat Button entity."""
press_fn: Callable[[SwitcherType2Api, SwitcherBreezeRemote], SwitcherBaseResponse] press_fn: Callable[
[SwitcherApi, SwitcherBreezeRemote],
Coroutine[Any, Any, SwitcherBaseResponse],
]
supported: Callable[[SwitcherBreezeRemote], bool] supported: Callable[[SwitcherBreezeRemote], bool]
@@ -85,9 +90,10 @@ async def async_setup_entry(
async def async_add_buttons(coordinator: SwitcherDataUpdateCoordinator) -> None: async def async_add_buttons(coordinator: SwitcherDataUpdateCoordinator) -> None:
"""Get remote and add button from Switcher device.""" """Get remote and add button from Switcher device."""
data = cast(SwitcherBreezeRemote, coordinator.data)
if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT: if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT:
remote: SwitcherBreezeRemote = await hass.async_add_executor_job( remote: SwitcherBreezeRemote = await hass.async_add_executor_job(
get_breeze_remote_manager(hass).get_remote, coordinator.data.remote_id get_breeze_remote_manager(hass).get_remote, data.remote_id
) )
async_add_entities( async_add_entities(
SwitcherThermostatButtonEntity(coordinator, description, remote) SwitcherThermostatButtonEntity(coordinator, description, remote)
@@ -126,7 +132,7 @@ class SwitcherThermostatButtonEntity(
async def async_press(self) -> None: async def async_press(self) -> None:
"""Press the button.""" """Press the button."""
response: SwitcherBaseResponse = None response: SwitcherBaseResponse | None = None
error = None error = None
try: try:

View File

@@ -9,6 +9,7 @@ from aioswitcher.api.remotes import SwitcherBreezeRemote
from aioswitcher.device import ( from aioswitcher.device import (
DeviceCategory, DeviceCategory,
DeviceState, DeviceState,
SwitcherThermostat,
ThermostatFanLevel, ThermostatFanLevel,
ThermostatMode, ThermostatMode,
ThermostatSwing, ThermostatSwing,
@@ -68,9 +69,10 @@ async def async_setup_entry(
async def async_add_climate(coordinator: SwitcherDataUpdateCoordinator) -> None: async def async_add_climate(coordinator: SwitcherDataUpdateCoordinator) -> None:
"""Get remote and add climate from Switcher device.""" """Get remote and add climate from Switcher device."""
data = cast(SwitcherThermostat, coordinator.data)
if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT: if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT:
remote: SwitcherBreezeRemote = await hass.async_add_executor_job( remote: SwitcherBreezeRemote = await hass.async_add_executor_job(
get_breeze_remote_manager(hass).get_remote, coordinator.data.remote_id get_breeze_remote_manager(hass).get_remote, data.remote_id
) )
async_add_entities([SwitcherClimateEntity(coordinator, remote)]) async_add_entities([SwitcherClimateEntity(coordinator, remote)])
@@ -133,13 +135,13 @@ class SwitcherClimateEntity(
def _update_data(self, force_update: bool = False) -> None: def _update_data(self, force_update: bool = False) -> None:
"""Update data from device.""" """Update data from device."""
data = self.coordinator.data data = cast(SwitcherThermostat, self.coordinator.data)
features = self._remote.modes_features[data.mode] features = self._remote.modes_features[data.mode]
if data.target_temperature == 0 and not force_update: if data.target_temperature == 0 and not force_update:
return return
self._attr_current_temperature = cast(float, data.temperature) self._attr_current_temperature = data.temperature
self._attr_target_temperature = float(data.target_temperature) self._attr_target_temperature = float(data.target_temperature)
self._attr_hvac_mode = HVACMode.OFF self._attr_hvac_mode = HVACMode.OFF
@@ -162,7 +164,7 @@ class SwitcherClimateEntity(
async def _async_control_breeze_device(self, **kwargs: Any) -> None: async def _async_control_breeze_device(self, **kwargs: Any) -> None:
"""Call Switcher Control Breeze API.""" """Call Switcher Control Breeze API."""
response: SwitcherBaseResponse = None response: SwitcherBaseResponse | None = None
error = None error = None
try: try:
@@ -185,9 +187,8 @@ class SwitcherClimateEntity(
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
if not self._remote.modes_features[self.coordinator.data.mode][ data = cast(SwitcherThermostat, self.coordinator.data)
"temperature_control" if not self._remote.modes_features[data.mode]["temperature_control"]:
]:
raise HomeAssistantError( raise HomeAssistantError(
"Current mode doesn't support setting Target Temperature" "Current mode doesn't support setting Target Temperature"
) )
@@ -199,7 +200,8 @@ class SwitcherClimateEntity(
async def async_set_fan_mode(self, fan_mode: str) -> None: async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode.""" """Set new target fan mode."""
if not self._remote.modes_features[self.coordinator.data.mode]["fan_levels"]: data = cast(SwitcherThermostat, self.coordinator.data)
if not self._remote.modes_features[data.mode]["fan_levels"]:
raise HomeAssistantError("Current mode doesn't support setting Fan Mode") raise HomeAssistantError("Current mode doesn't support setting Fan Mode")
await self._async_control_breeze_device(fan_level=HA_TO_DEVICE_FAN[fan_mode]) await self._async_control_breeze_device(fan_level=HA_TO_DEVICE_FAN[fan_mode])
@@ -215,7 +217,8 @@ class SwitcherClimateEntity(
async def async_set_swing_mode(self, swing_mode: str) -> None: async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing operation.""" """Set new target swing operation."""
if not self._remote.modes_features[self.coordinator.data.mode]["swing"]: data = cast(SwitcherThermostat, self.coordinator.data)
if not self._remote.modes_features[data.mode]["swing"]:
raise HomeAssistantError("Current mode doesn't support setting Swing Mode") raise HomeAssistantError("Current mode doesn't support setting Swing Mode")
if swing_mode == SWING_VERTICAL: if swing_mode == SWING_VERTICAL:

View File

@@ -45,17 +45,17 @@ class SwitcherDataUpdateCoordinator(
@property @property
def model(self) -> str: def model(self) -> str:
"""Switcher device model.""" """Switcher device model."""
return self.data.device_type.value # type: ignore[no-any-return] return self.data.device_type.value
@property @property
def device_id(self) -> str: def device_id(self) -> str:
"""Switcher device id.""" """Switcher device id."""
return self.data.device_id # type: ignore[no-any-return] return self.data.device_id
@property @property
def mac_address(self) -> str: def mac_address(self) -> str:
"""Switcher device mac address.""" """Switcher device mac address."""
return self.data.mac_address # type: ignore[no-any-return] return self.data.mac_address
@callback @callback
def async_setup(self) -> None: def async_setup(self) -> None:

View File

@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any from typing import Any, cast
from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api
from aioswitcher.device import DeviceCategory, ShutterDirection, SwitcherShutter from aioswitcher.device import DeviceCategory, ShutterDirection, SwitcherShutter
@@ -84,7 +84,7 @@ class SwitcherCoverEntity(
def _update_data(self) -> None: def _update_data(self) -> None:
"""Update data from device.""" """Update data from device."""
data: SwitcherShutter = self.coordinator.data data = cast(SwitcherShutter, self.coordinator.data)
self._attr_current_cover_position = data.position self._attr_current_cover_position = data.position
self._attr_is_closed = data.position == 0 self._attr_is_closed = data.position == 0
self._attr_is_closing = data.direction == ShutterDirection.SHUTTER_DOWN self._attr_is_closing = data.direction == ShutterDirection.SHUTTER_DOWN
@@ -93,7 +93,7 @@ class SwitcherCoverEntity(
async def _async_call_api(self, api: str, *args: Any) -> None: async def _async_call_api(self, api: str, *args: Any) -> None:
"""Call Switcher API.""" """Call Switcher API."""
_LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args) _LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args)
response: SwitcherBaseResponse = None response: SwitcherBaseResponse | None = None
error = None error = None
try: try:

View File

@@ -7,6 +7,6 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aioswitcher"], "loggers": ["aioswitcher"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aioswitcher==3.4.1"], "requirements": ["aioswitcher==3.4.3"],
"single_config_entry": true "single_config_entry": true
} }

View File

@@ -111,7 +111,7 @@ class SwitcherBaseSwitchEntity(
_LOGGER.debug( _LOGGER.debug(
"Calling api for %s, api: '%s', args: %s", self.coordinator.name, api, args "Calling api for %s, api: '%s', args: %s", self.coordinator.name, api, args
) )
response: SwitcherBaseResponse = None response: SwitcherBaseResponse | None = None
error = None error = None
try: try:

View File

@@ -28,13 +28,17 @@ from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
PLATFORMS: Final = [ PLATFORMS: Final = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE, Platform.CLIMATE,
Platform.COVER, Platform.COVER,
Platform.DEVICE_TRACKER, Platform.DEVICE_TRACKER,
Platform.LOCK, Platform.LOCK,
Platform.MEDIA_PLAYER,
Platform.NUMBER,
Platform.SELECT, Platform.SELECT,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
Platform.UPDATE,
] ]

View File

@@ -0,0 +1,85 @@
"""Button platform for Teslemetry integration."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from tesla_fleet_api.const import Scope
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import TeslemetryVehicleEntity
from .models import TeslemetryVehicleData
@dataclass(frozen=True, kw_only=True)
class TeslemetryButtonEntityDescription(ButtonEntityDescription):
"""Describes a Teslemetry Button entity."""
func: Callable[[TeslemetryButtonEntity], Awaitable[Any]] | None = None
DESCRIPTIONS: tuple[TeslemetryButtonEntityDescription, ...] = (
TeslemetryButtonEntityDescription(key="wake"), # Every button runs wakeup
TeslemetryButtonEntityDescription(
key="flash_lights", func=lambda self: self.api.flash_lights()
),
TeslemetryButtonEntityDescription(
key="honk", func=lambda self: self.api.honk_horn()
),
TeslemetryButtonEntityDescription(
key="enable_keyless_driving", func=lambda self: self.api.remote_start_drive()
),
TeslemetryButtonEntityDescription(
key="boombox", func=lambda self: self.api.remote_boombox(0)
),
TeslemetryButtonEntityDescription(
key="homelink",
func=lambda self: self.api.trigger_homelink(
lat=self.coordinator.data["drive_state_latitude"],
lon=self.coordinator.data["drive_state_longitude"],
),
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Teslemetry Button platform from a config entry."""
async_add_entities(
TeslemetryButtonEntity(vehicle, description)
for vehicle in entry.runtime_data.vehicles
for description in DESCRIPTIONS
if Scope.VEHICLE_CMDS in entry.runtime_data.scopes
)
class TeslemetryButtonEntity(TeslemetryVehicleEntity, ButtonEntity):
"""Base class for Teslemetry buttons."""
entity_description: TeslemetryButtonEntityDescription
def __init__(
self,
data: TeslemetryVehicleData,
description: TeslemetryButtonEntityDescription,
) -> None:
"""Initialize the button."""
self.entity_description = description
super().__init__(data, description.key)
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
async def async_press(self) -> None:
"""Press the button."""
await self.wake_up_if_asleep()
if self.entity_description.func:
await self.handle_command(self.entity_description.func(self))

View File

@@ -60,6 +60,12 @@ class TeslemetryEntity(
"""Return a specific value from coordinator data.""" """Return a specific value from coordinator data."""
return self.coordinator.data.get(key, default) return self.coordinator.data.get(key, default)
def get_number(self, key: str, default: float) -> float:
"""Return a specific number from coordinator data."""
if isinstance(value := self.coordinator.data.get(key), (int, float)):
return value
return default
@property @property
def is_none(self) -> bool: def is_none(self) -> bool:
"""Return if the value is a literal None.""" """Return if the value is a literal None."""

View File

@@ -38,6 +38,26 @@
} }
} }
}, },
"button": {
"boombox": {
"default": "mdi:volume-high"
},
"enable_keyless_driving": {
"default": "mdi:car-key"
},
"flash_lights": {
"default": "mdi:flashlight"
},
"homelink": {
"default": "mdi:garage"
},
"honk": {
"default": "mdi:bullhorn"
},
"wake": {
"default": "mdi:sleep-off"
}
},
"climate": { "climate": {
"driver_temp": { "driver_temp": {
"state_attributes": { "state_attributes": {

View File

@@ -0,0 +1,149 @@
"""Media player platform for Teslemetry integration."""
from __future__ import annotations
from tesla_fleet_api.const import Scope
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import TeslemetryVehicleEntity
from .models import TeslemetryVehicleData
STATES = {
"Playing": MediaPlayerState.PLAYING,
"Paused": MediaPlayerState.PAUSED,
"Stopped": MediaPlayerState.IDLE,
"Off": MediaPlayerState.OFF,
}
VOLUME_MAX = 11.0
VOLUME_STEP = 1.0 / 3
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Teslemetry Media platform from a config entry."""
async_add_entities(
TeslemetryMediaEntity(vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes)
for vehicle in entry.runtime_data.vehicles
)
class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity):
"""Vehicle media player class."""
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_supported_features = (
MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.PAUSE
| MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.VOLUME_SET
)
_volume_max: float = VOLUME_MAX
def __init__(
self,
data: TeslemetryVehicleData,
scoped: bool,
) -> None:
"""Initialize the media player entity."""
super().__init__(data, "media")
self.scoped = scoped
if not scoped:
self._attr_supported_features = MediaPlayerEntityFeature(0)
def _async_update_attrs(self) -> None:
"""Update entity attributes."""
self._volume_max = (
self.get("vehicle_state_media_info_audio_volume_max") or VOLUME_MAX
)
self._attr_state = STATES.get(
self.get("vehicle_state_media_info_media_playback_status") or "Off",
)
self._attr_volume_step = (
1.0
/ self._volume_max
/ (
self.get("vehicle_state_media_info_audio_volume_increment")
or VOLUME_STEP
)
)
if volume := self.get("vehicle_state_media_info_audio_volume"):
self._attr_volume_level = volume / self._volume_max
else:
self._attr_volume_level = None
if duration := self.get("vehicle_state_media_info_now_playing_duration"):
self._attr_media_duration = duration / 1000
else:
self._attr_media_duration = None
if duration and (
position := self.get("vehicle_state_media_info_now_playing_elapsed")
):
self._attr_media_position = position / 1000
else:
self._attr_media_position = None
self._attr_media_title = self.get("vehicle_state_media_info_now_playing_title")
self._attr_media_artist = self.get(
"vehicle_state_media_info_now_playing_artist"
)
self._attr_media_album_name = self.get(
"vehicle_state_media_info_now_playing_album"
)
self._attr_media_playlist = self.get(
"vehicle_state_media_info_now_playing_station"
)
self._attr_source = self.get("vehicle_state_media_info_now_playing_source")
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(
self.api.adjust_volume(int(volume * self._volume_max))
)
self._attr_volume_level = volume
self.async_write_ha_state()
async def async_media_play(self) -> None:
"""Send play command."""
if self.state != MediaPlayerState.PLAYING:
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.media_toggle_playback())
self._attr_state = MediaPlayerState.PLAYING
self.async_write_ha_state()
async def async_media_pause(self) -> None:
"""Send pause command."""
if self.state == MediaPlayerState.PLAYING:
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.media_toggle_playback())
self._attr_state = MediaPlayerState.PAUSED
self.async_write_ha_state()
async def async_media_next_track(self) -> None:
"""Send next track command."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.media_next_track())
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.media_prev_track())

View File

@@ -0,0 +1,201 @@
"""Number platform for Teslemetry integration."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from itertools import chain
from typing import Any
from tesla_fleet_api import EnergySpecific, VehicleSpecific
from tesla_fleet_api.const import Scope
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, PRECISION_WHOLE, UnitOfElectricCurrent
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.icon import icon_for_battery_level
from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity
from .models import TeslemetryEnergyData, TeslemetryVehicleData
@dataclass(frozen=True, kw_only=True)
class TeslemetryNumberVehicleEntityDescription(NumberEntityDescription):
"""Describes Teslemetry Number entity."""
func: Callable[[VehicleSpecific, float], Awaitable[Any]]
native_min_value: float
native_max_value: float
min_key: str | None = None
max_key: str
scopes: list[Scope]
VEHICLE_DESCRIPTIONS: tuple[TeslemetryNumberVehicleEntityDescription, ...] = (
TeslemetryNumberVehicleEntityDescription(
key="charge_state_charge_current_request",
native_step=PRECISION_WHOLE,
native_min_value=0,
native_max_value=32,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=NumberDeviceClass.CURRENT,
mode=NumberMode.AUTO,
max_key="charge_state_charge_current_request_max",
func=lambda api, value: api.set_charging_amps(value),
scopes=[Scope.VEHICLE_CHARGING_CMDS],
),
TeslemetryNumberVehicleEntityDescription(
key="charge_state_charge_limit_soc",
native_step=PRECISION_WHOLE,
native_min_value=50,
native_max_value=100,
native_unit_of_measurement=PERCENTAGE,
device_class=NumberDeviceClass.BATTERY,
mode=NumberMode.AUTO,
min_key="charge_state_charge_limit_soc_min",
max_key="charge_state_charge_limit_soc_max",
func=lambda api, value: api.set_charge_limit(value),
scopes=[Scope.VEHICLE_CHARGING_CMDS, Scope.VEHICLE_CMDS],
),
)
@dataclass(frozen=True, kw_only=True)
class TeslemetryNumberBatteryEntityDescription(NumberEntityDescription):
"""Describes Teslemetry Number entity."""
func: Callable[[EnergySpecific, float], Awaitable[Any]]
requires: str | None = None
ENERGY_INFO_DESCRIPTIONS: tuple[TeslemetryNumberBatteryEntityDescription, ...] = (
TeslemetryNumberBatteryEntityDescription(
key="backup_reserve_percent",
func=lambda api, value: api.backup(int(value)),
requires="components_battery",
),
TeslemetryNumberBatteryEntityDescription(
key="off_grid_vehicle_charging_reserve",
func=lambda api, value: api.off_grid_vehicle_charging_reserve(int(value)),
requires="components_off_grid_vehicle_charging_reserve_supported",
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Teslemetry number platform from a config entry."""
async_add_entities(
chain(
( # Add vehicle entities
TeslemetryVehicleNumberEntity(
vehicle,
description,
entry.runtime_data.scopes,
)
for vehicle in entry.runtime_data.vehicles
for description in VEHICLE_DESCRIPTIONS
),
( # Add energy site entities
TeslemetryEnergyInfoNumberSensorEntity(
energysite,
description,
entry.runtime_data.scopes,
)
for energysite in entry.runtime_data.energysites
for description in ENERGY_INFO_DESCRIPTIONS
if description.requires is None
or energysite.info_coordinator.data.get(description.requires)
),
)
)
class TeslemetryVehicleNumberEntity(TeslemetryVehicleEntity, NumberEntity):
"""Vehicle number entity base class."""
entity_description: TeslemetryNumberVehicleEntityDescription
def __init__(
self,
data: TeslemetryVehicleData,
description: TeslemetryNumberVehicleEntityDescription,
scopes: list[Scope],
) -> None:
"""Initialize the number entity."""
self.scoped = any(scope in scopes for scope in description.scopes)
self.entity_description = description
super().__init__(
data,
description.key,
)
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
self._attr_native_value = self._value
if (min_key := self.entity_description.min_key) is not None:
self._attr_native_min_value = self.get_number(
min_key,
self.entity_description.native_min_value,
)
else:
self._attr_native_min_value = self.entity_description.native_min_value
self._attr_native_max_value = self.get_number(
self.entity_description.max_key,
self.entity_description.native_max_value,
)
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
value = int(value)
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.entity_description.func(self.api, value))
self._attr_native_value = value
self.async_write_ha_state()
class TeslemetryEnergyInfoNumberSensorEntity(TeslemetryEnergyInfoEntity, NumberEntity):
"""Energy info number entity base class."""
entity_description: TeslemetryNumberBatteryEntityDescription
_attr_native_step = PRECISION_WHOLE
_attr_native_min_value = 0
_attr_native_max_value = 100
_attr_device_class = NumberDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
def __init__(
self,
data: TeslemetryEnergyData,
description: TeslemetryNumberBatteryEntityDescription,
scopes: list[Scope],
) -> None:
"""Initialize the number entity."""
self.scoped = Scope.ENERGY_CMDS in scopes
self.entity_description = description
super().__init__(data, description.key)
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
self._attr_native_value = self._value
self._attr_icon = icon_for_battery_level(self.native_value)
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
value = int(value)
self.raise_for_scope()
await self.handle_command(self.entity_description.func(self.api, value))
self._attr_native_value = value
self.async_write_ha_state()

View File

@@ -96,6 +96,26 @@
"name": "Tire pressure warning rear right" "name": "Tire pressure warning rear right"
} }
}, },
"button": {
"boombox": {
"name": "Play fart"
},
"enable_keyless_driving": {
"name": "Keyless driving"
},
"flash_lights": {
"name": "Flash lights"
},
"homelink": {
"name": "Homelink"
},
"honk": {
"name": "Honk horn"
},
"wake": {
"name": "Wake"
}
},
"climate": { "climate": {
"driver_temp": { "driver_temp": {
"name": "[%key:component::climate::title%]", "name": "[%key:component::climate::title%]",
@@ -219,6 +239,25 @@
} }
} }
}, },
"media_player": {
"media": {
"name": "[%key:component::media_player::title%]"
}
},
"number": {
"backup_reserve_percent": {
"name": "Backup reserve"
},
"charge_state_charge_current_request": {
"name": "Charge current"
},
"charge_state_charge_limit_soc": {
"name": "Charge limit"
},
"off_grid_vehicle_charging_reserve": {
"name": "Off grid reserve"
}
},
"cover": { "cover": {
"charge_state_charge_port_door_open": { "charge_state_charge_port_door_open": {
"name": "Charge port door" "name": "Charge port door"
@@ -415,6 +454,11 @@
"vehicle_state_valet_mode": { "vehicle_state_valet_mode": {
"name": "Valet mode" "name": "Valet mode"
} }
},
"update": {
"vehicle_state_software_update_status": {
"name": "[%key:component::update::title%]"
}
} }
}, },
"exceptions": { "exceptions": {

View File

@@ -0,0 +1,105 @@
"""Update platform for Teslemetry integration."""
from __future__ import annotations
from typing import Any, cast
from tesla_fleet_api.const import Scope
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import TeslemetryVehicleEntity
from .models import TeslemetryVehicleData
AVAILABLE = "available"
DOWNLOADING = "downloading"
INSTALLING = "installing"
WIFI_WAIT = "downloading_wifi_wait"
SCHEDULED = "scheduled"
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Teslemetry update platform from a config entry."""
async_add_entities(
TeslemetryUpdateEntity(vehicle, entry.runtime_data.scopes)
for vehicle in entry.runtime_data.vehicles
)
class TeslemetryUpdateEntity(TeslemetryVehicleEntity, UpdateEntity):
"""Teslemetry Updates entity."""
def __init__(
self,
data: TeslemetryVehicleData,
scopes: list[Scope],
) -> None:
"""Initialize the Update."""
self.scoped = Scope.VEHICLE_CMDS in scopes
super().__init__(
data,
"vehicle_state_software_update_status",
)
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
# Supported Features
if self.scoped and self._value in (
AVAILABLE,
SCHEDULED,
):
# Only allow install when an update has been fully downloaded
self._attr_supported_features = (
UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL
)
else:
self._attr_supported_features = UpdateEntityFeature.PROGRESS
# Installed Version
self._attr_installed_version = self.get("vehicle_state_car_version")
if self._attr_installed_version is not None:
# Remove build from version
self._attr_installed_version = self._attr_installed_version.split(" ")[0]
# Latest Version
if self._value in (
AVAILABLE,
SCHEDULED,
INSTALLING,
DOWNLOADING,
WIFI_WAIT,
):
self._attr_latest_version = self.coordinator.data[
"vehicle_state_software_update_version"
]
else:
self._attr_latest_version = self._attr_installed_version
# In Progress
if self._value in (
SCHEDULED,
INSTALLING,
):
self._attr_in_progress = (
cast(int, self.get("vehicle_state_software_update_install_perc"))
or True
)
else:
self._attr_in_progress = False
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.schedule_software_update(offset_sec=60))
self._attr_in_progress = True
self.async_write_ha_state()

View File

@@ -157,7 +157,7 @@ SCRIPT_DEBUG_CONTINUE_STOP: SignalTypeFormat[Literal["continue", "stop"]] = (
) )
SCRIPT_DEBUG_CONTINUE_ALL = "script_debug_continue_all" SCRIPT_DEBUG_CONTINUE_ALL = "script_debug_continue_all"
script_stack_cv: ContextVar[list[int] | None] = ContextVar("script_stack", default=None) script_stack_cv: ContextVar[list[str] | None] = ContextVar("script_stack", default=None)
class ScriptData(TypedDict): class ScriptData(TypedDict):
@@ -452,7 +452,7 @@ class _ScriptRun:
if (script_stack := script_stack_cv.get()) is None: if (script_stack := script_stack_cv.get()) is None:
script_stack = [] script_stack = []
script_stack_cv.set(script_stack) script_stack_cv.set(script_stack)
script_stack.append(id(self._script)) script_stack.append(self._script.unique_id)
response = None response = None
try: try:
@@ -1401,6 +1401,7 @@ class Script:
self.sequence = sequence self.sequence = sequence
template.attach(hass, self.sequence) template.attach(hass, self.sequence)
self.name = name self.name = name
self.unique_id = f"{domain}.{name}-{id(self)}"
self.domain = domain self.domain = domain
self.running_description = running_description or f"{domain} script" self.running_description = running_description or f"{domain} script"
self._change_listener = change_listener self._change_listener = change_listener
@@ -1723,10 +1724,21 @@ class Script:
if ( if (
self.script_mode in (SCRIPT_MODE_RESTART, SCRIPT_MODE_QUEUED) self.script_mode in (SCRIPT_MODE_RESTART, SCRIPT_MODE_QUEUED)
and script_stack is not None and script_stack is not None
and id(self) in script_stack and self.unique_id in script_stack
): ):
script_execution_set("disallowed_recursion_detected") script_execution_set("disallowed_recursion_detected")
self._log("Disallowed recursion detected", level=logging.WARNING) formatted_stack = [
f"- {name_id.partition('-')[0]}" for name_id in script_stack
]
self._log(
"Disallowed recursion detected, "
f"{script_stack[-1].partition('-')[0]} tried to start "
f"{self.domain}.{self.name} which is already running "
"in the current execution path; "
"Traceback (most recent call last):\n"
f"{"\n".join(formatted_stack)}",
level=logging.WARNING,
)
return None return None
if self.script_mode != SCRIPT_MODE_QUEUED: if self.script_mode != SCRIPT_MODE_QUEUED:

View File

@@ -327,7 +327,33 @@ def _false(arg: str) -> bool:
return False return False
_cached_literal_eval = lru_cache(maxsize=EVAL_CACHE_SIZE)(literal_eval) @lru_cache(maxsize=EVAL_CACHE_SIZE)
def _cached_parse_result(render_result: str) -> Any:
"""Parse a result and cache the result."""
result = literal_eval(render_result)
if type(result) in RESULT_WRAPPERS:
result = RESULT_WRAPPERS[type(result)](result, render_result=render_result)
# If the literal_eval result is a string, use the original
# render, by not returning right here. The evaluation of strings
# resulting in strings impacts quotes, to avoid unexpected
# output; use the original render instead of the evaluated one.
# Complex and scientific values are also unexpected. Filter them out.
if (
# Filter out string and complex numbers
not isinstance(result, (str, complex))
and (
# Pass if not numeric and not a boolean
not isinstance(result, (int, float))
# Or it's a boolean (inherit from int)
or isinstance(result, bool)
# Or if it's a digit
or _IS_NUMERIC.match(render_result) is not None
)
):
return result
return render_result
class RenderInfo: class RenderInfo:
@@ -588,31 +614,7 @@ class Template:
def _parse_result(self, render_result: str) -> Any: def _parse_result(self, render_result: str) -> Any:
"""Parse the result.""" """Parse the result."""
try: try:
result = _cached_literal_eval(render_result) return _cached_parse_result(render_result)
if type(result) in RESULT_WRAPPERS:
result = RESULT_WRAPPERS[type(result)](
result, render_result=render_result
)
# If the literal_eval result is a string, use the original
# render, by not returning right here. The evaluation of strings
# resulting in strings impacts quotes, to avoid unexpected
# output; use the original render instead of the evaluated one.
# Complex and scientific values are also unexpected. Filter them out.
if (
# Filter out string and complex numbers
not isinstance(result, (str, complex))
and (
# Pass if not numeric and not a boolean
not isinstance(result, (int, float))
# Or it's a boolean (inherit from int)
or isinstance(result, bool)
# Or if it's a digit
or _IS_NUMERIC.match(render_result) is not None
)
):
return result
except (ValueError, TypeError, SyntaxError, MemoryError): except (ValueError, TypeError, SyntaxError, MemoryError):
pass pass

View File

@@ -371,7 +371,7 @@ aiosolaredge==0.2.0
aiosteamist==0.3.2 aiosteamist==0.3.2
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==3.4.1 aioswitcher==3.4.3
# homeassistant.components.syncthing # homeassistant.components.syncthing
aiosyncthing==0.5.1 aiosyncthing==0.5.1
@@ -1646,7 +1646,7 @@ py-nightscout==1.2.2
py-schluter==0.1.7 py-schluter==0.1.7
# homeassistant.components.ecovacs # homeassistant.components.ecovacs
py-sucks==0.9.9 py-sucks==0.9.10
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==2.4.2 py-synologydsm-api==2.4.2
@@ -2650,7 +2650,7 @@ streamlabswater==1.0.1
stringcase==1.2.0 stringcase==1.2.0
# homeassistant.components.subaru # homeassistant.components.subaru
subarulink==0.7.9 subarulink==0.7.11
# homeassistant.components.solarlog # homeassistant.components.solarlog
sunwatcher==0.2.1 sunwatcher==0.2.1

View File

@@ -344,7 +344,7 @@ aiosolaredge==0.2.0
aiosteamist==0.3.2 aiosteamist==0.3.2
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==3.4.1 aioswitcher==3.4.3
# homeassistant.components.syncthing # homeassistant.components.syncthing
aiosyncthing==0.5.1 aiosyncthing==0.5.1
@@ -1308,7 +1308,7 @@ py-nextbusnext==1.0.2
py-nightscout==1.2.2 py-nightscout==1.2.2
# homeassistant.components.ecovacs # homeassistant.components.ecovacs
py-sucks==0.9.9 py-sucks==0.9.10
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==2.4.2 py-synologydsm-api==2.4.2
@@ -2063,7 +2063,7 @@ streamlabswater==1.0.1
stringcase==1.2.0 stringcase==1.2.0
# homeassistant.components.subaru # homeassistant.components.subaru
subarulink==0.7.9 subarulink==0.7.11
# homeassistant.components.solarlog # homeassistant.components.solarlog
sunwatcher==0.2.1 sunwatcher==0.2.1

View File

@@ -2,7 +2,7 @@
from ipaddress import ip_address from ipaddress import ip_address
from unittest import mock from unittest import mock
from unittest.mock import Mock, call, patch from unittest.mock import ANY, Mock, call, patch
import axis as axislib import axis as axislib
import pytest import pytest
@@ -90,7 +90,7 @@ async def test_device_support_mqtt(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry
) -> None: ) -> None:
"""Successful setup.""" """Successful setup."""
mqtt_call = call(f"axis/{MAC}/#", mock.ANY, 0, "utf-8") mqtt_call = call(f"axis/{MAC}/#", mock.ANY, 0, "utf-8", ANY)
assert mqtt_call in mqtt_mock.async_subscribe.call_args_list assert mqtt_call in mqtt_mock.async_subscribe.call_args_list
topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0"

View File

@@ -71,7 +71,6 @@ async def test_buttons(
{ATTR_ENTITY_ID: entity_id}, {ATTR_ENTITY_ID: entity_id},
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
mock_press_action.assert_called_once() mock_press_action.assert_called_once()
button = hass.states.get(entity_id) button = hass.states.get(entity_id)
@@ -105,7 +104,6 @@ async def test_wol_button(
{ATTR_ENTITY_ID: "button.printer_wake_on_lan"}, {ATTR_ENTITY_ID: "button.printer_wake_on_lan"},
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
mock_press_action.assert_called_once_with("AA:BB:CC:00:11:22") mock_press_action.assert_called_once_with("AA:BB:CC:00:11:22")
button = hass.states.get("button.printer_wake_on_lan") button = hass.states.get("button.printer_wake_on_lan")

View File

@@ -145,7 +145,6 @@ async def test_user(
== DEFAULT_CONSIDER_HOME.total_seconds() == DEFAULT_CONSIDER_HOME.total_seconds()
) )
assert not result["result"].unique_id assert not result["result"].unique_id
await hass.async_block_till_done()
assert mock_setup_entry.called assert mock_setup_entry.called
@@ -764,14 +763,12 @@ async def test_options_flow(hass: HomeAssistant) -> None:
mock_config.add_to_hass(hass) mock_config.add_to_hass(hass)
result = await hass.config_entries.options.async_init(mock_config.entry_id) result = await hass.config_entries.options.async_init(mock_config.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
CONF_CONSIDER_HOME: 37, CONF_CONSIDER_HOME: 37,
}, },
) )
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == { assert result["data"] == {

View File

@@ -56,7 +56,6 @@ async def test_options_reload(
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
result = await hass.config_entries.options.async_init(entry.entry_id) result = await hass.config_entries.options.async_init(entry.entry_id)
await hass.async_block_till_done()
await hass.config_entries.options.async_configure( await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input={CONF_CONSIDER_HOME: 60}, user_input={CONF_CONSIDER_HOME: 60},

View File

@@ -971,6 +971,36 @@ async def test_timers_not_supported(hass: HomeAssistant) -> None:
language=hass.config.language, language=hass.config.language,
) )
# Start a timer
@callback
def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None:
pass
device_id = "test_device"
unregister = timer_manager.register_handler(device_id, handle_timer)
timer_id = timer_manager.start_timer(
device_id,
hours=None,
minutes=5,
seconds=None,
language=hass.config.language,
)
# Unregister handler so device no longer "supports" timers
unregister()
# All operations on the timer should not crash
timer_manager.add_time(timer_id, 1)
timer_manager.remove_time(timer_id, 1)
timer_manager.pause_timer(timer_id)
timer_manager.unpause_timer(timer_id)
timer_manager.cancel_timer(timer_id)
async def test_timer_status_with_names(hass: HomeAssistant, init_components) -> None: async def test_timer_status_with_names(hass: HomeAssistant, init_components) -> None:
"""Test getting the status of named timers.""" """Test getting the status of named timers."""

View File

@@ -27,7 +27,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
EntityCategory, EntityCategory,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HassJobType, HomeAssistant
from homeassistant.generated.mqtt import MQTT from homeassistant.generated.mqtt import MQTT
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -1189,7 +1189,9 @@ async def help_test_entity_id_update_subscriptions(
assert state is not None assert state is not None
assert mqtt_mock.async_subscribe.call_count == len(topics) + 2 + DISCOVERY_COUNT assert mqtt_mock.async_subscribe.call_count == len(topics) + 2 + DISCOVERY_COUNT
for topic in topics: for topic in topics:
mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.assert_any_call(
topic, ANY, ANY, ANY, HassJobType.Callback
)
mqtt_mock.async_subscribe.reset_mock() mqtt_mock.async_subscribe.reset_mock()
entity_registry.async_update_entity( entity_registry.async_update_entity(
@@ -1203,7 +1205,9 @@ async def help_test_entity_id_update_subscriptions(
state = hass.states.get(f"{domain}.milk") state = hass.states.get(f"{domain}.milk")
assert state is not None assert state is not None
for topic in topics: for topic in topics:
mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.assert_any_call(
topic, ANY, ANY, ANY, HassJobType.Callback
)
async def help_test_entity_id_update_discovery_update( async def help_test_entity_id_update_discovery_update(

View File

@@ -154,7 +154,7 @@ async def test_qos_encoding_default(
{"test_topic1": {"topic": "test-topic1", "msg_callback": msg_callback}}, {"test_topic1": {"topic": "test-topic1", "msg_callback": msg_callback}},
) )
await async_subscribe_topics(hass, sub_state) await async_subscribe_topics(hass, sub_state)
mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 0, "utf-8") mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 0, "utf-8", None)
async def test_qos_encoding_custom( async def test_qos_encoding_custom(
@@ -183,7 +183,7 @@ async def test_qos_encoding_custom(
}, },
) )
await async_subscribe_topics(hass, sub_state) await async_subscribe_topics(hass, sub_state)
mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 1, "utf-16") mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 1, "utf-16", None)
async def test_no_change( async def test_no_change(

View File

@@ -6,7 +6,7 @@ import pytest
from homeassistant.components import automation from homeassistant.components import automation
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
from homeassistant.core import HomeAssistant from homeassistant.core import HassJobType, HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import async_fire_mqtt_message, async_mock_service, mock_component from tests.common import async_fire_mqtt_message, async_mock_service, mock_component
@@ -239,7 +239,9 @@ async def test_encoding_default(hass: HomeAssistant, calls, setup_comp) -> None:
}, },
) )
setup_comp.async_subscribe.assert_called_with("test-topic", ANY, 0, "utf-8") setup_comp.async_subscribe.assert_called_with(
"test-topic", ANY, 0, "utf-8", HassJobType.Callback
)
async def test_encoding_custom(hass: HomeAssistant, calls, setup_comp) -> None: async def test_encoding_custom(hass: HomeAssistant, calls, setup_comp) -> None:
@@ -255,4 +257,6 @@ async def test_encoding_custom(hass: HomeAssistant, calls, setup_comp) -> None:
}, },
) )
setup_comp.async_subscribe.assert_called_with("test-topic", ANY, 0, None) setup_comp.async_subscribe.assert_called_with(
"test-topic", ANY, 0, None, HassJobType.Callback
)

View File

@@ -66,7 +66,7 @@ async def test_subscribe(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No
await hass.async_block_till_done() await hass.async_block_till_done()
# Verify that the this entity was subscribed to the topic # Verify that the this entity was subscribed to the topic
mqtt_mock.async_subscribe.assert_called_with(sub_topic, ANY, 0, ANY) mqtt_mock.async_subscribe.assert_called_with(sub_topic, ANY, 0, ANY, ANY)
async def test_state_changed_event_sends_message( async def test_state_changed_event_sends_message(

View File

@@ -128,7 +128,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None:
user_input={CONF_API_KEY: "newkey"}, user_input={CONF_API_KEY: "newkey"},
) )
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful" assert result["reason"] == "reauth_successful"
assert entry.data[CONF_API_KEY] == "newkey" assert entry.data[CONF_API_KEY] == "newkey"

View File

@@ -199,8 +199,6 @@ async def test_disable_service_call(hass: HomeAssistant) -> None:
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
mocked_hole.disable.assert_called_with(1) mocked_hole.disable.assert_called_with(1)
@@ -219,8 +217,6 @@ async def test_unload(hass: HomeAssistant) -> None:
assert isinstance(entry.runtime_data, PiHoleData) assert isinstance(entry.runtime_data, PiHoleData)
assert await hass.config_entries.async_unload(entry.entry_id) assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED assert entry.state is ConfigEntryState.NOT_LOADED

View File

@@ -1741,3 +1741,46 @@ async def test_responses_no_response(hass: HomeAssistant) -> None:
) )
is None is None
) )
async def test_script_queued_mode(hass: HomeAssistant) -> None:
"""Test calling a queued mode script called in parallel."""
calls = 0
async def async_service_handler(*args, **kwargs) -> None:
"""Service that simulates doing background I/O."""
nonlocal calls
calls += 1
await asyncio.sleep(0)
hass.services.async_register("test", "simulated_remote", async_service_handler)
assert await async_setup_component(
hass,
script.DOMAIN,
{
script.DOMAIN: {
"test_main": {
"sequence": [
{
"parallel": [
{"service": "script.test_sub"},
{"service": "script.test_sub"},
{"service": "script.test_sub"},
{"service": "script.test_sub"},
]
}
]
},
"test_sub": {
"mode": "queued",
"sequence": [
{"service": "test.simulated_remote"},
],
},
}
},
)
await hass.async_block_till_done()
await hass.services.async_call("script", "test_main", blocking=True)
assert calls == 4

View File

@@ -62,19 +62,13 @@ MOCK_DATETIME = datetime.fromtimestamp(1595560000, UTC)
VEHICLE_STATUS_EV = { VEHICLE_STATUS_EV = {
VEHICLE_STATUS: { VEHICLE_STATUS: {
"AVG_FUEL_CONSUMPTION": 2.3, "AVG_FUEL_CONSUMPTION": 51.1,
"DISTANCE_TO_EMPTY_FUEL": 707, "DISTANCE_TO_EMPTY_FUEL": 170,
"DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
"DOOR_BOOT_POSITION": "CLOSED", "DOOR_BOOT_POSITION": "CLOSED",
"DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN",
"DOOR_ENGINE_HOOD_POSITION": "CLOSED", "DOOR_ENGINE_HOOD_POSITION": "CLOSED",
"DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN",
"DOOR_FRONT_LEFT_POSITION": "CLOSED", "DOOR_FRONT_LEFT_POSITION": "CLOSED",
"DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN",
"DOOR_FRONT_RIGHT_POSITION": "CLOSED", "DOOR_FRONT_RIGHT_POSITION": "CLOSED",
"DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN",
"DOOR_REAR_LEFT_POSITION": "CLOSED", "DOOR_REAR_LEFT_POSITION": "CLOSED",
"DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
"DOOR_REAR_RIGHT_POSITION": "CLOSED", "DOOR_REAR_RIGHT_POSITION": "CLOSED",
"EV_CHARGER_STATE_TYPE": "CHARGING", "EV_CHARGER_STATE_TYPE": "CHARGING",
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
@@ -85,37 +79,12 @@ VEHICLE_STATUS_EV = {
"EV_STATE_OF_CHARGE_PERCENT": 20, "EV_STATE_OF_CHARGE_PERCENT": 20,
"EV_TIME_TO_FULLY_CHARGED_UTC": MOCK_DATETIME, "EV_TIME_TO_FULLY_CHARGED_UTC": MOCK_DATETIME,
"ODOMETER": 1234, "ODOMETER": 1234,
"POSITION_HEADING_DEGREE": 150,
"POSITION_SPEED_KMPH": "0",
"POSITION_TIMESTAMP": 1595560000.0,
"SEAT_BELT_STATUS_FRONT_LEFT": "BELTED",
"SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
"SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED",
"SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN",
"SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN",
"SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN",
"SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN",
"SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN",
"SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
"SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN",
"TIMESTAMP": 1595560000.0, "TIMESTAMP": 1595560000.0,
"TRANSMISSION_MODE": "UNKNOWN", "TRANSMISSION_MODE": "UNKNOWN",
"TYRE_PRESSURE_FRONT_LEFT": 0, "TYRE_PRESSURE_FRONT_LEFT": 0.0,
"TYRE_PRESSURE_FRONT_RIGHT": 2550, "TYRE_PRESSURE_FRONT_RIGHT": 31.9,
"TYRE_PRESSURE_REAR_LEFT": 2450, "TYRE_PRESSURE_REAR_LEFT": 32.6,
"TYRE_PRESSURE_REAR_RIGHT": None, "TYRE_PRESSURE_REAR_RIGHT": None,
"TYRE_STATUS_FRONT_LEFT": "UNKNOWN",
"TYRE_STATUS_FRONT_RIGHT": "UNKNOWN",
"TYRE_STATUS_REAR_LEFT": "UNKNOWN",
"TYRE_STATUS_REAR_RIGHT": "UNKNOWN",
"VEHICLE_STATE_TYPE": "IGNITION_OFF", "VEHICLE_STATE_TYPE": "IGNITION_OFF",
"WINDOW_BACK_STATUS": "UNKNOWN", "WINDOW_BACK_STATUS": "UNKNOWN",
"WINDOW_FRONT_LEFT_STATUS": "VENTED", "WINDOW_FRONT_LEFT_STATUS": "VENTED",
@@ -123,7 +92,6 @@ VEHICLE_STATUS_EV = {
"WINDOW_REAR_LEFT_STATUS": "UNKNOWN", "WINDOW_REAR_LEFT_STATUS": "UNKNOWN",
"WINDOW_REAR_RIGHT_STATUS": "UNKNOWN", "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN",
"WINDOW_SUNROOF_STATUS": "UNKNOWN", "WINDOW_SUNROOF_STATUS": "UNKNOWN",
"HEADING": 170,
"LATITUDE": 40.0, "LATITUDE": 40.0,
"LONGITUDE": -100.0, "LONGITUDE": -100.0,
} }
@@ -132,53 +100,22 @@ VEHICLE_STATUS_EV = {
VEHICLE_STATUS_G3 = { VEHICLE_STATUS_G3 = {
VEHICLE_STATUS: { VEHICLE_STATUS: {
"AVG_FUEL_CONSUMPTION": 2.3, "AVG_FUEL_CONSUMPTION": 51.1,
"DISTANCE_TO_EMPTY_FUEL": 707, "DISTANCE_TO_EMPTY_FUEL": 170,
"DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
"DOOR_BOOT_POSITION": "CLOSED", "DOOR_BOOT_POSITION": "CLOSED",
"DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN",
"DOOR_ENGINE_HOOD_POSITION": "CLOSED", "DOOR_ENGINE_HOOD_POSITION": "CLOSED",
"DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN",
"DOOR_FRONT_LEFT_POSITION": "CLOSED", "DOOR_FRONT_LEFT_POSITION": "CLOSED",
"DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN",
"DOOR_FRONT_RIGHT_POSITION": "CLOSED", "DOOR_FRONT_RIGHT_POSITION": "CLOSED",
"DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN",
"DOOR_REAR_LEFT_POSITION": "CLOSED", "DOOR_REAR_LEFT_POSITION": "CLOSED",
"DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
"DOOR_REAR_RIGHT_POSITION": "CLOSED", "DOOR_REAR_RIGHT_POSITION": "CLOSED",
"REMAINING_FUEL_PERCENT": 77, "REMAINING_FUEL_PERCENT": 77,
"ODOMETER": 1234, "ODOMETER": 1234,
"POSITION_HEADING_DEGREE": 150,
"POSITION_SPEED_KMPH": "0",
"POSITION_TIMESTAMP": 1595560000.0,
"SEAT_BELT_STATUS_FRONT_LEFT": "BELTED",
"SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
"SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED",
"SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN",
"SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN",
"SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN",
"SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN",
"SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN",
"SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED",
"SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN",
"SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN",
"TIMESTAMP": 1595560000.0, "TIMESTAMP": 1595560000.0,
"TRANSMISSION_MODE": "UNKNOWN", "TRANSMISSION_MODE": "UNKNOWN",
"TYRE_PRESSURE_FRONT_LEFT": 2550, "TYRE_PRESSURE_FRONT_LEFT": 0.0,
"TYRE_PRESSURE_FRONT_RIGHT": 2550, "TYRE_PRESSURE_FRONT_RIGHT": 31.9,
"TYRE_PRESSURE_REAR_LEFT": 2450, "TYRE_PRESSURE_REAR_LEFT": 32.6,
"TYRE_PRESSURE_REAR_RIGHT": None, "TYRE_PRESSURE_REAR_RIGHT": None,
"TYRE_STATUS_FRONT_LEFT": "UNKNOWN",
"TYRE_STATUS_FRONT_RIGHT": "UNKNOWN",
"TYRE_STATUS_REAR_LEFT": "UNKNOWN",
"TYRE_STATUS_REAR_RIGHT": "UNKNOWN",
"VEHICLE_STATE_TYPE": "IGNITION_OFF", "VEHICLE_STATE_TYPE": "IGNITION_OFF",
"WINDOW_BACK_STATUS": "UNKNOWN", "WINDOW_BACK_STATUS": "UNKNOWN",
"WINDOW_FRONT_LEFT_STATUS": "VENTED", "WINDOW_FRONT_LEFT_STATUS": "VENTED",
@@ -186,15 +123,14 @@ VEHICLE_STATUS_G3 = {
"WINDOW_REAR_LEFT_STATUS": "UNKNOWN", "WINDOW_REAR_LEFT_STATUS": "UNKNOWN",
"WINDOW_REAR_RIGHT_STATUS": "UNKNOWN", "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN",
"WINDOW_SUNROOF_STATUS": "UNKNOWN", "WINDOW_SUNROOF_STATUS": "UNKNOWN",
"HEADING": 170,
"LATITUDE": 40.0, "LATITUDE": 40.0,
"LONGITUDE": -100.0, "LONGITUDE": -100.0,
} }
} }
EXPECTED_STATE_EV_IMPERIAL = { EXPECTED_STATE_EV_IMPERIAL = {
"AVG_FUEL_CONSUMPTION": "102.3", "AVG_FUEL_CONSUMPTION": "51.1",
"DISTANCE_TO_EMPTY_FUEL": "439.3", "DISTANCE_TO_EMPTY_FUEL": "170",
"EV_CHARGER_STATE_TYPE": "CHARGING", "EV_CHARGER_STATE_TYPE": "CHARGING",
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
"EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
@@ -203,45 +139,37 @@ EXPECTED_STATE_EV_IMPERIAL = {
"EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_MODE": "EV_MODE",
"EV_STATE_OF_CHARGE_PERCENT": "20", "EV_STATE_OF_CHARGE_PERCENT": "20",
"EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00", "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00",
"ODOMETER": "766.8", "ODOMETER": "1234",
"POSITION_HEADING_DEGREE": "150",
"POSITION_SPEED_KMPH": "0",
"POSITION_TIMESTAMP": 1595560000.0,
"TIMESTAMP": 1595560000.0, "TIMESTAMP": 1595560000.0,
"TRANSMISSION_MODE": "UNKNOWN", "TRANSMISSION_MODE": "UNKNOWN",
"TYRE_PRESSURE_FRONT_LEFT": "0.0", "TYRE_PRESSURE_FRONT_LEFT": "0.0",
"TYRE_PRESSURE_FRONT_RIGHT": "37.0", "TYRE_PRESSURE_FRONT_RIGHT": "31.9",
"TYRE_PRESSURE_REAR_LEFT": "35.5", "TYRE_PRESSURE_REAR_LEFT": "32.6",
"TYRE_PRESSURE_REAR_RIGHT": "unknown", "TYRE_PRESSURE_REAR_RIGHT": "unknown",
"VEHICLE_STATE_TYPE": "IGNITION_OFF", "VEHICLE_STATE_TYPE": "IGNITION_OFF",
"HEADING": 170,
"LATITUDE": 40.0, "LATITUDE": 40.0,
"LONGITUDE": -100.0, "LONGITUDE": -100.0,
} }
EXPECTED_STATE_EV_METRIC = { EXPECTED_STATE_EV_METRIC = {
"AVG_FUEL_CONSUMPTION": "2.3", "AVG_FUEL_CONSUMPTION": "4.6",
"DISTANCE_TO_EMPTY_FUEL": "707", "DISTANCE_TO_EMPTY_FUEL": "274",
"EV_CHARGER_STATE_TYPE": "CHARGING", "EV_CHARGER_STATE_TYPE": "CHARGING",
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
"EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1",
"EV_DISTANCE_TO_EMPTY": "1.6", "EV_DISTANCE_TO_EMPTY": "2",
"EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED",
"EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_MODE": "EV_MODE",
"EV_STATE_OF_CHARGE_PERCENT": "20", "EV_STATE_OF_CHARGE_PERCENT": "20",
"EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00", "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00",
"ODOMETER": "1234", "ODOMETER": "1986",
"POSITION_HEADING_DEGREE": "150",
"POSITION_SPEED_KMPH": "0",
"POSITION_TIMESTAMP": 1595560000.0,
"TIMESTAMP": 1595560000.0, "TIMESTAMP": 1595560000.0,
"TRANSMISSION_MODE": "UNKNOWN", "TRANSMISSION_MODE": "UNKNOWN",
"TYRE_PRESSURE_FRONT_LEFT": "0", "TYRE_PRESSURE_FRONT_LEFT": "0.0",
"TYRE_PRESSURE_FRONT_RIGHT": "2550", "TYRE_PRESSURE_FRONT_RIGHT": "219.9",
"TYRE_PRESSURE_REAR_LEFT": "2450", "TYRE_PRESSURE_REAR_LEFT": "224.8",
"TYRE_PRESSURE_REAR_RIGHT": "unknown", "TYRE_PRESSURE_REAR_RIGHT": "unknown",
"VEHICLE_STATE_TYPE": "IGNITION_OFF", "VEHICLE_STATE_TYPE": "IGNITION_OFF",
"HEADING": 170,
"LATITUDE": 40.0, "LATITUDE": 40.0,
"LONGITUDE": -100.0, "LONGITUDE": -100.0,
} }
@@ -259,9 +187,6 @@ EXPECTED_STATE_EV_UNAVAILABLE = {
"EV_STATE_OF_CHARGE_PERCENT": "unavailable", "EV_STATE_OF_CHARGE_PERCENT": "unavailable",
"EV_TIME_TO_FULLY_CHARGED_UTC": "unavailable", "EV_TIME_TO_FULLY_CHARGED_UTC": "unavailable",
"ODOMETER": "unavailable", "ODOMETER": "unavailable",
"POSITION_HEADING_DEGREE": "unavailable",
"POSITION_SPEED_KMPH": "unavailable",
"POSITION_TIMESTAMP": "unavailable",
"TIMESTAMP": "unavailable", "TIMESTAMP": "unavailable",
"TRANSMISSION_MODE": "unavailable", "TRANSMISSION_MODE": "unavailable",
"TYRE_PRESSURE_FRONT_LEFT": "unavailable", "TYRE_PRESSURE_FRONT_LEFT": "unavailable",
@@ -269,7 +194,6 @@ EXPECTED_STATE_EV_UNAVAILABLE = {
"TYRE_PRESSURE_REAR_LEFT": "unavailable", "TYRE_PRESSURE_REAR_LEFT": "unavailable",
"TYRE_PRESSURE_REAR_RIGHT": "unavailable", "TYRE_PRESSURE_REAR_RIGHT": "unavailable",
"VEHICLE_STATE_TYPE": "unavailable", "VEHICLE_STATE_TYPE": "unavailable",
"HEADING": "unavailable",
"LATITUDE": "unavailable", "LATITUDE": "unavailable",
"LONGITUDE": "unavailable", "LONGITUDE": "unavailable",
} }

View File

@@ -11,19 +11,13 @@
'data': list([ 'data': list([
dict({ dict({
'vehicle_status': dict({ 'vehicle_status': dict({
'AVG_FUEL_CONSUMPTION': 2.3, 'AVG_FUEL_CONSUMPTION': 51.1,
'DISTANCE_TO_EMPTY_FUEL': 707, 'DISTANCE_TO_EMPTY_FUEL': 170,
'DOOR_BOOT_LOCK_STATUS': 'UNKNOWN',
'DOOR_BOOT_POSITION': 'CLOSED', 'DOOR_BOOT_POSITION': 'CLOSED',
'DOOR_ENGINE_HOOD_LOCK_STATUS': 'UNKNOWN',
'DOOR_ENGINE_HOOD_POSITION': 'CLOSED', 'DOOR_ENGINE_HOOD_POSITION': 'CLOSED',
'DOOR_FRONT_LEFT_LOCK_STATUS': 'UNKNOWN',
'DOOR_FRONT_LEFT_POSITION': 'CLOSED', 'DOOR_FRONT_LEFT_POSITION': 'CLOSED',
'DOOR_FRONT_RIGHT_LOCK_STATUS': 'UNKNOWN',
'DOOR_FRONT_RIGHT_POSITION': 'CLOSED', 'DOOR_FRONT_RIGHT_POSITION': 'CLOSED',
'DOOR_REAR_LEFT_LOCK_STATUS': 'UNKNOWN',
'DOOR_REAR_LEFT_POSITION': 'CLOSED', 'DOOR_REAR_LEFT_POSITION': 'CLOSED',
'DOOR_REAR_RIGHT_LOCK_STATUS': 'UNKNOWN',
'DOOR_REAR_RIGHT_POSITION': 'CLOSED', 'DOOR_REAR_RIGHT_POSITION': 'CLOSED',
'EV_CHARGER_STATE_TYPE': 'CHARGING', 'EV_CHARGER_STATE_TYPE': 'CHARGING',
'EV_CHARGE_SETTING_AMPERE_TYPE': 'MAXIMUM', 'EV_CHARGE_SETTING_AMPERE_TYPE': 'MAXIMUM',
@@ -33,41 +27,15 @@
'EV_STATE_OF_CHARGE_MODE': 'EV_MODE', 'EV_STATE_OF_CHARGE_MODE': 'EV_MODE',
'EV_STATE_OF_CHARGE_PERCENT': 20, 'EV_STATE_OF_CHARGE_PERCENT': 20,
'EV_TIME_TO_FULLY_CHARGED_UTC': '2020-07-24T03:06:40+00:00', 'EV_TIME_TO_FULLY_CHARGED_UTC': '2020-07-24T03:06:40+00:00',
'HEADING': 170,
'LATITUDE': '**REDACTED**', 'LATITUDE': '**REDACTED**',
'LONGITUDE': '**REDACTED**', 'LONGITUDE': '**REDACTED**',
'ODOMETER': '**REDACTED**', 'ODOMETER': '**REDACTED**',
'POSITION_HEADING_DEGREE': 150,
'POSITION_SPEED_KMPH': '0',
'POSITION_TIMESTAMP': 1595560000.0,
'SEAT_BELT_STATUS_FRONT_LEFT': 'BELTED',
'SEAT_BELT_STATUS_FRONT_MIDDLE': 'NOT_EQUIPPED',
'SEAT_BELT_STATUS_FRONT_RIGHT': 'BELTED',
'SEAT_BELT_STATUS_SECOND_LEFT': 'UNKNOWN',
'SEAT_BELT_STATUS_SECOND_MIDDLE': 'UNKNOWN',
'SEAT_BELT_STATUS_SECOND_RIGHT': 'UNKNOWN',
'SEAT_BELT_STATUS_THIRD_LEFT': 'UNKNOWN',
'SEAT_BELT_STATUS_THIRD_MIDDLE': 'UNKNOWN',
'SEAT_BELT_STATUS_THIRD_RIGHT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_FRONT_LEFT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_FRONT_MIDDLE': 'NOT_EQUIPPED',
'SEAT_OCCUPATION_STATUS_FRONT_RIGHT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_SECOND_LEFT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_SECOND_MIDDLE': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_SECOND_RIGHT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_THIRD_LEFT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_THIRD_MIDDLE': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_THIRD_RIGHT': 'UNKNOWN',
'TIMESTAMP': 1595560000.0, 'TIMESTAMP': 1595560000.0,
'TRANSMISSION_MODE': 'UNKNOWN', 'TRANSMISSION_MODE': 'UNKNOWN',
'TYRE_PRESSURE_FRONT_LEFT': 0, 'TYRE_PRESSURE_FRONT_LEFT': 0.0,
'TYRE_PRESSURE_FRONT_RIGHT': 2550, 'TYRE_PRESSURE_FRONT_RIGHT': 31.9,
'TYRE_PRESSURE_REAR_LEFT': 2450, 'TYRE_PRESSURE_REAR_LEFT': 32.6,
'TYRE_PRESSURE_REAR_RIGHT': None, 'TYRE_PRESSURE_REAR_RIGHT': None,
'TYRE_STATUS_FRONT_LEFT': 'UNKNOWN',
'TYRE_STATUS_FRONT_RIGHT': 'UNKNOWN',
'TYRE_STATUS_REAR_LEFT': 'UNKNOWN',
'TYRE_STATUS_REAR_RIGHT': 'UNKNOWN',
'VEHICLE_STATE_TYPE': 'IGNITION_OFF', 'VEHICLE_STATE_TYPE': 'IGNITION_OFF',
'WINDOW_BACK_STATUS': 'UNKNOWN', 'WINDOW_BACK_STATUS': 'UNKNOWN',
'WINDOW_FRONT_LEFT_STATUS': 'VENTED', 'WINDOW_FRONT_LEFT_STATUS': 'VENTED',
@@ -94,19 +62,13 @@
}), }),
'data': dict({ 'data': dict({
'vehicle_status': dict({ 'vehicle_status': dict({
'AVG_FUEL_CONSUMPTION': 2.3, 'AVG_FUEL_CONSUMPTION': 51.1,
'DISTANCE_TO_EMPTY_FUEL': 707, 'DISTANCE_TO_EMPTY_FUEL': 170,
'DOOR_BOOT_LOCK_STATUS': 'UNKNOWN',
'DOOR_BOOT_POSITION': 'CLOSED', 'DOOR_BOOT_POSITION': 'CLOSED',
'DOOR_ENGINE_HOOD_LOCK_STATUS': 'UNKNOWN',
'DOOR_ENGINE_HOOD_POSITION': 'CLOSED', 'DOOR_ENGINE_HOOD_POSITION': 'CLOSED',
'DOOR_FRONT_LEFT_LOCK_STATUS': 'UNKNOWN',
'DOOR_FRONT_LEFT_POSITION': 'CLOSED', 'DOOR_FRONT_LEFT_POSITION': 'CLOSED',
'DOOR_FRONT_RIGHT_LOCK_STATUS': 'UNKNOWN',
'DOOR_FRONT_RIGHT_POSITION': 'CLOSED', 'DOOR_FRONT_RIGHT_POSITION': 'CLOSED',
'DOOR_REAR_LEFT_LOCK_STATUS': 'UNKNOWN',
'DOOR_REAR_LEFT_POSITION': 'CLOSED', 'DOOR_REAR_LEFT_POSITION': 'CLOSED',
'DOOR_REAR_RIGHT_LOCK_STATUS': 'UNKNOWN',
'DOOR_REAR_RIGHT_POSITION': 'CLOSED', 'DOOR_REAR_RIGHT_POSITION': 'CLOSED',
'EV_CHARGER_STATE_TYPE': 'CHARGING', 'EV_CHARGER_STATE_TYPE': 'CHARGING',
'EV_CHARGE_SETTING_AMPERE_TYPE': 'MAXIMUM', 'EV_CHARGE_SETTING_AMPERE_TYPE': 'MAXIMUM',
@@ -116,41 +78,15 @@
'EV_STATE_OF_CHARGE_MODE': 'EV_MODE', 'EV_STATE_OF_CHARGE_MODE': 'EV_MODE',
'EV_STATE_OF_CHARGE_PERCENT': 20, 'EV_STATE_OF_CHARGE_PERCENT': 20,
'EV_TIME_TO_FULLY_CHARGED_UTC': '2020-07-24T03:06:40+00:00', 'EV_TIME_TO_FULLY_CHARGED_UTC': '2020-07-24T03:06:40+00:00',
'HEADING': 170,
'LATITUDE': '**REDACTED**', 'LATITUDE': '**REDACTED**',
'LONGITUDE': '**REDACTED**', 'LONGITUDE': '**REDACTED**',
'ODOMETER': '**REDACTED**', 'ODOMETER': '**REDACTED**',
'POSITION_HEADING_DEGREE': 150,
'POSITION_SPEED_KMPH': '0',
'POSITION_TIMESTAMP': 1595560000.0,
'SEAT_BELT_STATUS_FRONT_LEFT': 'BELTED',
'SEAT_BELT_STATUS_FRONT_MIDDLE': 'NOT_EQUIPPED',
'SEAT_BELT_STATUS_FRONT_RIGHT': 'BELTED',
'SEAT_BELT_STATUS_SECOND_LEFT': 'UNKNOWN',
'SEAT_BELT_STATUS_SECOND_MIDDLE': 'UNKNOWN',
'SEAT_BELT_STATUS_SECOND_RIGHT': 'UNKNOWN',
'SEAT_BELT_STATUS_THIRD_LEFT': 'UNKNOWN',
'SEAT_BELT_STATUS_THIRD_MIDDLE': 'UNKNOWN',
'SEAT_BELT_STATUS_THIRD_RIGHT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_FRONT_LEFT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_FRONT_MIDDLE': 'NOT_EQUIPPED',
'SEAT_OCCUPATION_STATUS_FRONT_RIGHT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_SECOND_LEFT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_SECOND_MIDDLE': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_SECOND_RIGHT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_THIRD_LEFT': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_THIRD_MIDDLE': 'UNKNOWN',
'SEAT_OCCUPATION_STATUS_THIRD_RIGHT': 'UNKNOWN',
'TIMESTAMP': 1595560000.0, 'TIMESTAMP': 1595560000.0,
'TRANSMISSION_MODE': 'UNKNOWN', 'TRANSMISSION_MODE': 'UNKNOWN',
'TYRE_PRESSURE_FRONT_LEFT': 0, 'TYRE_PRESSURE_FRONT_LEFT': 0.0,
'TYRE_PRESSURE_FRONT_RIGHT': 2550, 'TYRE_PRESSURE_FRONT_RIGHT': 31.9,
'TYRE_PRESSURE_REAR_LEFT': 2450, 'TYRE_PRESSURE_REAR_LEFT': 32.6,
'TYRE_PRESSURE_REAR_RIGHT': None, 'TYRE_PRESSURE_REAR_RIGHT': None,
'TYRE_STATUS_FRONT_LEFT': 'UNKNOWN',
'TYRE_STATUS_FRONT_RIGHT': 'UNKNOWN',
'TYRE_STATUS_REAR_LEFT': 'UNKNOWN',
'TYRE_STATUS_REAR_RIGHT': 'UNKNOWN',
'VEHICLE_STATE_TYPE': 'IGNITION_OFF', 'VEHICLE_STATE_TYPE': 'IGNITION_OFF',
'WINDOW_BACK_STATUS': 'UNKNOWN', 'WINDOW_BACK_STATUS': 'UNKNOWN',
'WINDOW_FRONT_LEFT_STATUS': 'VENTED', 'WINDOW_FRONT_LEFT_STATUS': 'VENTED',

View File

@@ -14,14 +14,11 @@ from homeassistant.components.subaru.sensor import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from .api_responses import ( from .api_responses import (
EXPECTED_STATE_EV_IMPERIAL,
EXPECTED_STATE_EV_METRIC, EXPECTED_STATE_EV_METRIC,
EXPECTED_STATE_EV_UNAVAILABLE, EXPECTED_STATE_EV_UNAVAILABLE,
TEST_VIN_2_EV, TEST_VIN_2_EV,
VEHICLE_STATUS_EV,
) )
from .conftest import ( from .conftest import (
MOCK_API_FETCH, MOCK_API_FETCH,
@@ -31,20 +28,6 @@ from .conftest import (
) )
async def test_sensors_ev_imperial(hass: HomeAssistant, ev_entry) -> None:
"""Test sensors supporting imperial units."""
hass.config.units = US_CUSTOMARY_SYSTEM
with (
patch(MOCK_API_FETCH),
patch(MOCK_API_GET_DATA, return_value=VEHICLE_STATUS_EV),
):
advance_time_to_next_fetch(hass)
await hass.async_block_till_done()
_assert_data(hass, EXPECTED_STATE_EV_IMPERIAL)
async def test_sensors_ev_metric(hass: HomeAssistant, ev_entry) -> None: async def test_sensors_ev_metric(hass: HomeAssistant, ev_entry) -> None:
"""Test sensors supporting metric units.""" """Test sensors supporting metric units."""
_assert_data(hass, EXPECTED_STATE_EV_METRIC) _assert_data(hass, EXPECTED_STATE_EV_METRIC)

View File

@@ -693,7 +693,7 @@ async def help_test_entity_id_update_subscriptions(
assert state is not None assert state is not None
assert mqtt_mock.async_subscribe.call_count == len(topics) assert mqtt_mock.async_subscribe.call_count == len(topics)
for topic in topics: for topic in topics:
mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY, ANY)
mqtt_mock.async_subscribe.reset_mock() mqtt_mock.async_subscribe.reset_mock()
entity_reg.async_update_entity( entity_reg.async_update_entity(
@@ -707,7 +707,7 @@ async def help_test_entity_id_update_subscriptions(
state = hass.states.get(f"{domain}.milk") state = hass.states.get(f"{domain}.milk")
assert state is not None assert state is not None
for topic in topics: for topic in topics:
mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY, ANY)
async def help_test_entity_id_update_discovery_update( async def help_test_entity_id_update_discovery_update(

View File

@@ -30,7 +30,9 @@ async def test_subscribing_config_topic(
discovery_topic = DEFAULT_PREFIX discovery_topic = DEFAULT_PREFIX
assert mqtt_mock.async_subscribe.called assert mqtt_mock.async_subscribe.called
mqtt_mock.async_subscribe.assert_any_call(discovery_topic + "/#", ANY, 0, "utf-8") mqtt_mock.async_subscribe.assert_any_call(
discovery_topic + "/#", ANY, 0, "utf-8", ANY
)
async def test_future_discovery_message( async def test_future_discovery_message(

View File

@@ -26,7 +26,7 @@
"storm_mode_capable": true, "storm_mode_capable": true,
"flex_energy_request_capable": false, "flex_energy_request_capable": false,
"car_charging_data_supported": false, "car_charging_data_supported": false,
"off_grid_vehicle_charging_reserve_supported": false, "off_grid_vehicle_charging_reserve_supported": true,
"vehicle_charging_performance_view_enabled": false, "vehicle_charging_performance_view_enabled": false,
"vehicle_charging_solar_offset_view_enabled": false, "vehicle_charging_solar_offset_view_enabled": false,
"battery_solar_offset_view_enabled": true, "battery_solar_offset_view_enabled": true,

View File

@@ -204,17 +204,18 @@
"is_user_present": false, "is_user_present": false,
"locked": false, "locked": false,
"media_info": { "media_info": {
"audio_volume": 2.6667, "a2dp_source_name": "Pixel 8 Pro",
"audio_volume": 1.6667,
"audio_volume_increment": 0.333333, "audio_volume_increment": 0.333333,
"audio_volume_max": 10.333333, "audio_volume_max": 10.333333,
"media_playback_status": "Stopped", "media_playback_status": "Playing",
"now_playing_album": "", "now_playing_album": "Elon Musk",
"now_playing_artist": "", "now_playing_artist": "Walter Isaacson",
"now_playing_duration": 0, "now_playing_duration": 651000,
"now_playing_elapsed": 0, "now_playing_elapsed": 1000,
"now_playing_source": "Spotify", "now_playing_source": "Audible",
"now_playing_station": "", "now_playing_station": "Elon Musk",
"now_playing_title": "" "now_playing_title": "Chapter 51: Cybertruck: Tesla, 20182019"
}, },
"media_state": { "media_state": {
"remote_control_enabled": true "remote_control_enabled": true
@@ -236,11 +237,11 @@
"service_mode": false, "service_mode": false,
"service_mode_plus": false, "service_mode_plus": false,
"software_update": { "software_update": {
"download_perc": 0, "download_perc": 100,
"expected_duration_sec": 2700, "expected_duration_sec": 2700,
"install_perc": 1, "install_perc": 1,
"status": "", "status": "available",
"version": " " "version": "2024.12.0.0"
}, },
"speed_limit_mode": { "speed_limit_mode": {
"active": false, "active": false,

View File

@@ -0,0 +1,323 @@
# serializer version: 1
# name: test_button[button.test_flash_lights-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.test_flash_lights',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Flash lights',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'flash_lights',
'unique_id': 'VINVINVIN-flash_lights',
'unit_of_measurement': None,
})
# ---
# name: test_button[button.test_flash_lights-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Flash lights',
}),
'context': <ANY>,
'entity_id': 'button.test_flash_lights',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_button[button.test_force_refresh-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.test_force_refresh',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Force refresh',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'refresh',
'unique_id': 'VINVINVIN-refresh',
'unit_of_measurement': None,
})
# ---
# name: test_button[button.test_force_refresh-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Force refresh',
}),
'context': <ANY>,
'entity_id': 'button.test_force_refresh',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_button[button.test_homelink-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.test_homelink',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Homelink',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'homelink',
'unique_id': 'VINVINVIN-homelink',
'unit_of_measurement': None,
})
# ---
# name: test_button[button.test_homelink-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Homelink',
}),
'context': <ANY>,
'entity_id': 'button.test_homelink',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_button[button.test_honk_horn-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.test_honk_horn',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Honk horn',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'honk',
'unique_id': 'VINVINVIN-honk',
'unit_of_measurement': None,
})
# ---
# name: test_button[button.test_honk_horn-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Honk horn',
}),
'context': <ANY>,
'entity_id': 'button.test_honk_horn',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_button[button.test_keyless_driving-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.test_keyless_driving',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Keyless driving',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'enable_keyless_driving',
'unique_id': 'VINVINVIN-enable_keyless_driving',
'unit_of_measurement': None,
})
# ---
# name: test_button[button.test_keyless_driving-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Keyless driving',
}),
'context': <ANY>,
'entity_id': 'button.test_keyless_driving',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_button[button.test_play_fart-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.test_play_fart',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Play fart',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'boombox',
'unique_id': 'VINVINVIN-boombox',
'unit_of_measurement': None,
})
# ---
# name: test_button[button.test_play_fart-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Play fart',
}),
'context': <ANY>,
'entity_id': 'button.test_play_fart',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_button[button.test_wake-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.test_wake',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Wake',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'wake',
'unique_id': 'VINVINVIN-wake',
'unit_of_measurement': None,
})
# ---
# name: test_button[button.test_wake-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Wake',
}),
'context': <ANY>,
'entity_id': 'button.test_wake',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@@ -62,7 +62,7 @@
'components_grid_services_enabled': False, 'components_grid_services_enabled': False,
'components_load_meter': True, 'components_load_meter': True,
'components_net_meter_mode': 'battery_ok', 'components_net_meter_mode': 'battery_ok',
'components_off_grid_vehicle_charging_reserve_supported': False, 'components_off_grid_vehicle_charging_reserve_supported': True,
'components_set_islanding_mode_enabled': True, 'components_set_islanding_mode_enabled': True,
'components_show_grid_import_battery_source_cards': True, 'components_show_grid_import_battery_source_cards': True,
'components_solar': True, 'components_solar': True,
@@ -361,17 +361,18 @@
'vehicle_state_ft': 0, 'vehicle_state_ft': 0,
'vehicle_state_is_user_present': False, 'vehicle_state_is_user_present': False,
'vehicle_state_locked': False, 'vehicle_state_locked': False,
'vehicle_state_media_info_audio_volume': 2.6667, 'vehicle_state_media_info_a2dp_source_name': 'Pixel 8 Pro',
'vehicle_state_media_info_audio_volume': 1.6667,
'vehicle_state_media_info_audio_volume_increment': 0.333333, 'vehicle_state_media_info_audio_volume_increment': 0.333333,
'vehicle_state_media_info_audio_volume_max': 10.333333, 'vehicle_state_media_info_audio_volume_max': 10.333333,
'vehicle_state_media_info_media_playback_status': 'Stopped', 'vehicle_state_media_info_media_playback_status': 'Playing',
'vehicle_state_media_info_now_playing_album': '', 'vehicle_state_media_info_now_playing_album': 'Elon Musk',
'vehicle_state_media_info_now_playing_artist': '', 'vehicle_state_media_info_now_playing_artist': 'Walter Isaacson',
'vehicle_state_media_info_now_playing_duration': 0, 'vehicle_state_media_info_now_playing_duration': 651000,
'vehicle_state_media_info_now_playing_elapsed': 0, 'vehicle_state_media_info_now_playing_elapsed': 1000,
'vehicle_state_media_info_now_playing_source': 'Spotify', 'vehicle_state_media_info_now_playing_source': 'Audible',
'vehicle_state_media_info_now_playing_station': '', 'vehicle_state_media_info_now_playing_station': 'Elon Musk',
'vehicle_state_media_info_now_playing_title': '', 'vehicle_state_media_info_now_playing_title': 'Chapter 51: Cybertruck: Tesla, 20182019',
'vehicle_state_media_state_remote_control_enabled': True, 'vehicle_state_media_state_remote_control_enabled': True,
'vehicle_state_notifications_supported': True, 'vehicle_state_notifications_supported': True,
'vehicle_state_odometer': 6481.019282, 'vehicle_state_odometer': 6481.019282,
@@ -389,11 +390,11 @@
'vehicle_state_sentry_mode_available': True, 'vehicle_state_sentry_mode_available': True,
'vehicle_state_service_mode': False, 'vehicle_state_service_mode': False,
'vehicle_state_service_mode_plus': False, 'vehicle_state_service_mode_plus': False,
'vehicle_state_software_update_download_perc': 0, 'vehicle_state_software_update_download_perc': 100,
'vehicle_state_software_update_expected_duration_sec': 2700, 'vehicle_state_software_update_expected_duration_sec': 2700,
'vehicle_state_software_update_install_perc': 1, 'vehicle_state_software_update_install_perc': 1,
'vehicle_state_software_update_status': '', 'vehicle_state_software_update_status': 'available',
'vehicle_state_software_update_version': ' ', 'vehicle_state_software_update_version': '2024.12.0.0',
'vehicle_state_speed_limit_mode_active': False, 'vehicle_state_speed_limit_mode_active': False,
'vehicle_state_speed_limit_mode_current_limit_mph': 69, 'vehicle_state_speed_limit_mode_current_limit_mph': 69,
'vehicle_state_speed_limit_mode_max_limit_mph': 120, 'vehicle_state_speed_limit_mode_max_limit_mph': 120,

View File

@@ -0,0 +1,136 @@
# serializer version: 1
# name: test_media_player[media_player.test_media_player-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.test_media_player',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <MediaPlayerDeviceClass.SPEAKER: 'speaker'>,
'original_icon': None,
'original_name': 'Media player',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': <MediaPlayerEntityFeature: 16437>,
'translation_key': 'media',
'unique_id': 'VINVINVIN-media',
'unit_of_measurement': None,
})
# ---
# name: test_media_player[media_player.test_media_player-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speaker',
'friendly_name': 'Test Media player',
'media_album_name': 'Elon Musk',
'media_artist': 'Walter Isaacson',
'media_duration': 651.0,
'media_playlist': 'Elon Musk',
'media_position': 1.0,
'media_title': 'Chapter 51: Cybertruck: Tesla, 20182019',
'source': 'Audible',
'supported_features': <MediaPlayerEntityFeature: 16437>,
'volume_level': 0.16129355359011466,
}),
'context': <ANY>,
'entity_id': 'media_player.test_media_player',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_media_player_alt[media_player.test_media_player-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speaker',
'friendly_name': 'Test Media player',
'media_album_name': '',
'media_artist': '',
'media_playlist': '',
'media_title': '',
'source': 'Spotify',
'supported_features': <MediaPlayerEntityFeature: 16437>,
'volume_level': 0.25806775026025003,
}),
'context': <ANY>,
'entity_id': 'media_player.test_media_player',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'idle',
})
# ---
# name: test_media_player_noscope[media_player.test_media_player-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.test_media_player',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <MediaPlayerDeviceClass.SPEAKER: 'speaker'>,
'original_icon': None,
'original_name': 'Media player',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'media',
'unique_id': 'VINVINVIN-media',
'unit_of_measurement': None,
})
# ---
# name: test_media_player_noscope[media_player.test_media_player-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speaker',
'friendly_name': 'Test Media player',
'media_album_name': 'Elon Musk',
'media_artist': 'Walter Isaacson',
'media_duration': 651.0,
'media_playlist': 'Elon Musk',
'media_position': 1.0,
'media_title': 'Chapter 51: Cybertruck: Tesla, 20182019',
'source': 'Audible',
'supported_features': <MediaPlayerEntityFeature: 0>,
'volume_level': 0.16129355359011466,
}),
'context': <ANY>,
'entity_id': 'media_player.test_media_player',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---

View File

@@ -0,0 +1,461 @@
# serializer version: 1
# name: test_number[number.energy_site_backup_reserve-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.energy_site_backup_reserve',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': 'mdi:battery-alert',
'original_name': 'Backup reserve',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'backup_reserve_percent',
'unique_id': '123456-backup_reserve_percent',
'unit_of_measurement': '%',
})
# ---
# name: test_number[number.energy_site_backup_reserve-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Energy Site Backup reserve',
'icon': 'mdi:battery-alert',
'max': 100,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.energy_site_backup_reserve',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_number[number.energy_site_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.energy_site_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': 'mdi:battery-alert',
'original_name': 'Battery',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'backup_reserve_percent',
'unique_id': '123456-backup_reserve_percent',
'unit_of_measurement': '%',
})
# ---
# name: test_number[number.energy_site_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Energy Site Battery',
'icon': 'mdi:battery-alert',
'max': 100,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.energy_site_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_number[number.energy_site_battery_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.energy_site_battery_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': 'mdi:battery-unknown',
'original_name': 'Battery',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'off_grid_vehicle_charging_reserve',
'unique_id': '123456-off_grid_vehicle_charging_reserve',
'unit_of_measurement': '%',
})
# ---
# name: test_number[number.energy_site_battery_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Energy Site Battery',
'icon': 'mdi:battery-unknown',
'max': 100,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.energy_site_battery_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_number[number.energy_site_off_grid_reserve-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.energy_site_off_grid_reserve',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': 'mdi:battery-unknown',
'original_name': 'Off grid reserve',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'off_grid_vehicle_charging_reserve',
'unique_id': '123456-off_grid_vehicle_charging_reserve',
'unit_of_measurement': '%',
})
# ---
# name: test_number[number.energy_site_off_grid_reserve-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Energy Site Off grid reserve',
'icon': 'mdi:battery-unknown',
'max': 100,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.energy_site_off_grid_reserve',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_number[number.test_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 50,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.test_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'charge_state_charge_limit_soc',
'unique_id': 'VINVINVIN-charge_state_charge_limit_soc',
'unit_of_measurement': '%',
})
# ---
# name: test_number[number.test_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Test Battery',
'max': 100,
'min': 50,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.test_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '80',
})
# ---
# name: test_number[number.test_charge_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 16,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.test_charge_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.CURRENT: 'current'>,
'original_icon': None,
'original_name': 'Charge current',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'charge_state_charge_current_request',
'unique_id': 'VINVINVIN-charge_state_charge_current_request',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
# ---
# name: test_number[number.test_charge_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': 'Test Charge current',
'max': 16,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
'entity_id': 'number.test_charge_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '16',
})
# ---
# name: test_number[number.test_charge_limit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 50,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.test_charge_limit',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Charge limit',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'charge_state_charge_limit_soc',
'unique_id': 'VINVINVIN-charge_state_charge_limit_soc',
'unit_of_measurement': '%',
})
# ---
# name: test_number[number.test_charge_limit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Test Charge limit',
'max': 100,
'min': 50,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.test_charge_limit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '80',
})
# ---
# name: test_number[number.test_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 16,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.test_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.CURRENT: 'current'>,
'original_icon': None,
'original_name': 'Current',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'charge_state_charge_current_request',
'unique_id': 'VINVINVIN-charge_state_charge_current_request',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
# ---
# name: test_number[number.test_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': 'Test Current',
'max': 16,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
'entity_id': 'number.test_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '16',
})
# ---

View File

@@ -0,0 +1,113 @@
# serializer version: 1
# name: test_update[update.test_update-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'update',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'update.test_update',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Update',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': <UpdateEntityFeature: 5>,
'translation_key': 'vehicle_state_software_update_status',
'unique_id': 'VINVINVIN-vehicle_state_software_update_status',
'unit_of_measurement': None,
})
# ---
# name: test_update[update.test_update-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'entity_picture': 'https://brands.home-assistant.io/_/teslemetry/icon.png',
'friendly_name': 'Test Update',
'in_progress': False,
'installed_version': '2023.44.30.8',
'latest_version': '2024.12.0.0',
'release_summary': None,
'release_url': None,
'skipped_version': None,
'supported_features': <UpdateEntityFeature: 5>,
'title': None,
}),
'context': <ANY>,
'entity_id': 'update.test_update',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_update_alt[update.test_update-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'update',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'update.test_update',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Update',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': <UpdateEntityFeature: 4>,
'translation_key': 'vehicle_state_software_update_status',
'unique_id': 'VINVINVIN-vehicle_state_software_update_status',
'unit_of_measurement': None,
})
# ---
# name: test_update_alt[update.test_update-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'entity_picture': 'https://brands.home-assistant.io/_/teslemetry/icon.png',
'friendly_name': 'Test Update',
'in_progress': False,
'installed_version': '2023.44.30.8',
'latest_version': '2023.44.30.8',
'release_summary': None,
'release_url': None,
'skipped_version': None,
'supported_features': <UpdateEntityFeature: 4>,
'title': None,
}),
'context': <ANY>,
'entity_id': 'update.test_update',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@@ -0,0 +1,53 @@
"""Test the Teslemetry button platform."""
from unittest.mock import patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import assert_entities, setup_platform
from .const import COMMAND_OK
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_button(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Tests that the button entities are correct."""
entry = await setup_platform(hass, [Platform.BUTTON])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
@pytest.mark.parametrize(
("name", "func"),
[
("flash_lights", "flash_lights"),
("honk_horn", "honk_horn"),
("keyless_driving", "remote_start_drive"),
("play_fart", "remote_boombox"),
("homelink", "trigger_homelink"),
],
)
async def test_press(hass: HomeAssistant, name: str, func: str) -> None:
"""Test pressing the API buttons."""
await setup_platform(hass, [Platform.BUTTON])
with patch(
f"homeassistant.components.teslemetry.VehicleSpecific.{func}",
return_value=COMMAND_OK,
) as command:
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: [f"button.test_{name}"]},
blocking=True,
)
command.assert_called_once()

View File

@@ -0,0 +1,152 @@
"""Test the Teslemetry media player platform."""
from unittest.mock import patch
from syrupy import SnapshotAssertion
from tesla_fleet_api.exceptions import VehicleOffline
from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_LEVEL,
DOMAIN as MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_VOLUME_SET,
MediaPlayerState,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import assert_entities, assert_entities_alt, setup_platform
from .const import COMMAND_OK, METADATA_NOSCOPE, VEHICLE_DATA_ALT
async def test_media_player(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Tests that the media player entities are correct."""
entry = await setup_platform(hass, [Platform.MEDIA_PLAYER])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
async def test_media_player_alt(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_vehicle_data,
) -> None:
"""Tests that the media player entities are correct."""
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
entry = await setup_platform(hass, [Platform.MEDIA_PLAYER])
assert_entities_alt(hass, entry.entry_id, entity_registry, snapshot)
async def test_media_player_offline(
hass: HomeAssistant,
mock_vehicle_data,
) -> None:
"""Tests that the media player entities are correct when offline."""
mock_vehicle_data.side_effect = VehicleOffline
await setup_platform(hass, [Platform.MEDIA_PLAYER])
state = hass.states.get("media_player.test_media_player")
assert state.state == MediaPlayerState.OFF
async def test_media_player_noscope(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_metadata,
) -> None:
"""Tests that the media player entities are correct without required scope."""
mock_metadata.return_value = METADATA_NOSCOPE
entry = await setup_platform(hass, [Platform.MEDIA_PLAYER])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
async def test_media_player_services(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Tests that the media player services work."""
await setup_platform(hass, [Platform.MEDIA_PLAYER])
entity_id = "media_player.test_media_player"
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.adjust_volume",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_SET,
{ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_LEVEL: 0.5},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.5
call.assert_called_once()
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.media_toggle_playback",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PAUSE,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == MediaPlayerState.PAUSED
call.assert_called_once()
# This test will fail without the previous call to pause playback
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.media_toggle_playback",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PLAY,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == MediaPlayerState.PLAYING
call.assert_called_once()
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.media_next_track",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_NEXT_TRACK,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
state = hass.states.get(entity_id)
call.assert_called_once()
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.media_prev_track",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PREVIOUS_TRACK,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
state = hass.states.get(entity_id)
call.assert_called_once()

View File

@@ -0,0 +1,113 @@
"""Test the Teslemetry number platform."""
from unittest.mock import patch
import pytest
from syrupy import SnapshotAssertion
from tesla_fleet_api.exceptions import VehicleOffline
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import assert_entities, setup_platform
from .const import COMMAND_OK, VEHICLE_DATA_ALT
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_number(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Tests that the number entities are correct."""
entry = await setup_platform(hass, [Platform.NUMBER])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
async def test_number_offline(
hass: HomeAssistant,
mock_vehicle_data,
) -> None:
"""Tests that the number entities are correct when offline."""
mock_vehicle_data.side_effect = VehicleOffline
await setup_platform(hass, [Platform.NUMBER])
state = hass.states.get("number.test_charge_current")
assert state.state == STATE_UNKNOWN
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_number_services(hass: HomeAssistant, mock_vehicle_data) -> None:
"""Tests that the number services work."""
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
await setup_platform(hass, [Platform.NUMBER])
entity_id = "number.test_charge_current"
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.set_charging_amps",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 16},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == "16"
call.assert_called_once()
entity_id = "number.test_charge_limit"
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.set_charge_limit",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 60},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == "60"
call.assert_called_once()
entity_id = "number.energy_site_backup_reserve"
with patch(
"homeassistant.components.teslemetry.EnergySpecific.backup",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: entity_id,
ATTR_VALUE: 80,
},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == "80"
call.assert_called_once()
entity_id = "number.energy_site_off_grid_reserve"
with patch(
"homeassistant.components.teslemetry.EnergySpecific.off_grid_vehicle_charging_reserve",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 88},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == "88"
call.assert_called_once()

View File

@@ -0,0 +1,89 @@
"""Test the Teslemetry update platform."""
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
from syrupy import SnapshotAssertion
from tesla_fleet_api.exceptions import VehicleOffline
from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL
from homeassistant.components.teslemetry.update import INSTALLING
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import assert_entities, setup_platform
from .const import COMMAND_OK, VEHICLE_DATA, VEHICLE_DATA_ALT
from tests.common import async_fire_time_changed
async def test_update(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Tests that the update entities are correct."""
entry = await setup_platform(hass, [Platform.UPDATE])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
async def test_update_alt(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_vehicle_data,
) -> None:
"""Tests that the update entities are correct."""
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
entry = await setup_platform(hass, [Platform.UPDATE])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
async def test_update_offline(
hass: HomeAssistant,
mock_vehicle_data,
) -> None:
"""Tests that the update entities are correct when offline."""
mock_vehicle_data.side_effect = VehicleOffline
await setup_platform(hass, [Platform.UPDATE])
state = hass.states.get("update.test_update")
assert state.state == STATE_UNKNOWN
async def test_update_services(
hass: HomeAssistant,
mock_vehicle_data,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
) -> None:
"""Tests that the update services work."""
await setup_platform(hass, [Platform.UPDATE])
entity_id = "update.test_update"
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.schedule_software_update",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
UPDATE_DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
call.assert_called_once()
VEHICLE_DATA["response"]["vehicle_state"]["software_update"]["status"] = INSTALLING
mock_vehicle_data.return_value = VEHICLE_DATA
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.attributes["in_progress"] == 1

View File

@@ -6247,3 +6247,72 @@ async def test_stopping_run_before_starting(
# would hang indefinitely. # would hang indefinitely.
run = script._ScriptRun(hass, script_obj, {}, None, True) run = script._ScriptRun(hass, script_obj, {}, None, True)
await run.async_stop() await run.async_stop()
async def test_disallowed_recursion(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test a queued mode script disallowed recursion."""
context = Context()
calls = 0
alias = "event step"
sequence1 = cv.SCRIPT_SCHEMA({"alias": alias, "service": "test.call_script_2"})
script1_obj = script.Script(
hass,
sequence1,
"Test Name1",
"test_domain1",
script_mode="queued",
running_description="test script1",
)
sequence2 = cv.SCRIPT_SCHEMA({"alias": alias, "service": "test.call_script_3"})
script2_obj = script.Script(
hass,
sequence2,
"Test Name2",
"test_domain2",
script_mode="queued",
running_description="test script2",
)
sequence3 = cv.SCRIPT_SCHEMA({"alias": alias, "service": "test.call_script_1"})
script3_obj = script.Script(
hass,
sequence3,
"Test Name3",
"test_domain3",
script_mode="queued",
running_description="test script3",
)
async def _async_service_handler_1(*args, **kwargs) -> None:
await script1_obj.async_run(context=context)
hass.services.async_register("test", "call_script_1", _async_service_handler_1)
async def _async_service_handler_2(*args, **kwargs) -> None:
await script2_obj.async_run(context=context)
hass.services.async_register("test", "call_script_2", _async_service_handler_2)
async def _async_service_handler_3(*args, **kwargs) -> None:
await script3_obj.async_run(context=context)
hass.services.async_register("test", "call_script_3", _async_service_handler_3)
await script1_obj.async_run(context=context)
await hass.async_block_till_done()
assert calls == 0
assert (
"Test Name1: Disallowed recursion detected, "
"test_domain3.Test Name3 tried to start test_domain1.Test Name1"
" which is already running in the current execution path; "
"Traceback (most recent call last):"
) in caplog.text
assert (
"- test_domain1.Test Name1\n"
"- test_domain2.Test Name2\n"
"- test_domain3.Test Name3"
) in caplog.text