mirror of
https://github.com/home-assistant/core.git
synced 2025-08-05 13:45:12 +02:00
Merge branch 'dev' into jbouwh-mqtt-device-discovery
This commit is contained in:
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"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"]
|
||||
}
|
||||
|
@@ -292,7 +292,8 @@ class TimerManager:
|
||||
|
||||
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(
|
||||
"Timer cancelled: id=%s, name=%s, seconds_left=%s, device_id=%s",
|
||||
timer_id,
|
||||
@@ -320,7 +321,8 @@ class TimerManager:
|
||||
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:
|
||||
log_verb = "increased"
|
||||
@@ -357,7 +359,8 @@ class TimerManager:
|
||||
task = self.timer_tasks.pop(timer_id)
|
||||
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(
|
||||
"Timer paused: id=%s, name=%s, seconds_left=%s, device_id=%s",
|
||||
timer_id,
|
||||
@@ -382,7 +385,8 @@ class TimerManager:
|
||||
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(
|
||||
"Timer unpaused: id=%s, name=%s, seconds_left=%s, device_id=%s",
|
||||
timer_id,
|
||||
@@ -397,7 +401,8 @@ class TimerManager:
|
||||
|
||||
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(
|
||||
"Timer finished: id=%s, name=%s, device_id=%s",
|
||||
timer_id,
|
||||
|
@@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HassJobType, HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -220,6 +220,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -309,13 +310,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
|
||||
"""Publish via mqtt."""
|
||||
variables = {"action": action, "code": code}
|
||||
payload = self._command_template(None, variables=variables)
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
|
||||
def _validate_code(self, code: str | None, state: str) -> bool:
|
||||
"""Validate given code."""
|
||||
|
@@ -26,7 +26,7 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
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
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.helpers.event as evt
|
||||
@@ -248,6 +248,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@@ -14,13 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
)
|
||||
from .const import CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_RETAIN
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MqttCommandTemplate
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
@@ -91,10 +85,4 @@ class MqttButton(MqttEntity, ButtonEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
payload = self._command_template(self._config[CONF_PAYLOAD_PRESS])
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
|
@@ -13,7 +13,7 @@ from homeassistant.components import camera
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -124,6 +124,7 @@ class MqttCamera(MqttEntity, Camera):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@@ -201,6 +201,7 @@ def async_subscribe_internal(
|
||||
msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None],
|
||||
qos: int = DEFAULT_QOS,
|
||||
encoding: str | None = DEFAULT_ENCODING,
|
||||
job_type: HassJobType | None = None,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Subscribe to an MQTT topic.
|
||||
|
||||
@@ -228,7 +229,7 @@ def async_subscribe_internal(
|
||||
translation_domain=DOMAIN,
|
||||
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
|
||||
@@ -867,12 +868,14 @@ class MQTT:
|
||||
msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None],
|
||||
qos: int,
|
||||
encoding: str | None = None,
|
||||
job_type: HassJobType | None = None,
|
||||
) -> Callable[[], None]:
|
||||
"""Set up a subscription to a topic with the provided qos."""
|
||||
if not isinstance(topic, str):
|
||||
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:
|
||||
# Only wrap the callback with catch_log_exception
|
||||
# if it is not a simple callback since we catch
|
||||
|
@@ -43,7 +43,7 @@ from homeassistant.const import (
|
||||
PRECISION_WHOLE,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.template import Template
|
||||
@@ -429,6 +429,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": qos,
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
def render_template(
|
||||
@@ -515,13 +516,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||
|
||||
async def _publish(self, topic: str, payload: PublishPayloadType) -> None:
|
||||
if self._topic[topic] is not None:
|
||||
await self.async_publish(
|
||||
self._topic[topic],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._topic[topic], payload)
|
||||
|
||||
async def _set_climate_attribute(
|
||||
self,
|
||||
|
@@ -28,7 +28,7 @@ from homeassistant.const import (
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
|
||||
@@ -478,6 +478,7 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
if self._config.get(CONF_STATE_TOPIC):
|
||||
@@ -491,6 +492,7 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
if self._config.get(CONF_TILT_STATUS_TOPIC) is not None:
|
||||
@@ -504,6 +506,7 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
@@ -519,12 +522,8 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
self._config[CONF_PAYLOAD_OPEN],
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_OPEN]
|
||||
)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
@@ -538,12 +537,8 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
self._config[CONF_PAYLOAD_CLOSE],
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_CLOSE]
|
||||
)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
@@ -557,12 +552,8 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
self._config[CONF_PAYLOAD_STOP],
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_STOP]
|
||||
)
|
||||
|
||||
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_payload = self._set_tilt_template(tilt_open_position, variables=variables)
|
||||
await self.async_publish(
|
||||
self._config[CONF_TILT_COMMAND_TOPIC],
|
||||
tilt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_TILT_COMMAND_TOPIC], tilt_payload
|
||||
)
|
||||
if self._tilt_optimistic:
|
||||
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_closed_position, variables=variables
|
||||
)
|
||||
await self.async_publish(
|
||||
self._config[CONF_TILT_COMMAND_TOPIC],
|
||||
tilt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_TILT_COMMAND_TOPIC], tilt_payload
|
||||
)
|
||||
if self._tilt_optimistic:
|
||||
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_rendered = self._set_tilt_template(tilt_ranged, variables=variables)
|
||||
|
||||
await self.async_publish(
|
||||
self._config[CONF_TILT_COMMAND_TOPIC],
|
||||
tilt_rendered,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_TILT_COMMAND_TOPIC], tilt_rendered
|
||||
)
|
||||
if self._tilt_optimistic:
|
||||
_LOGGER.debug("Set tilt value optimistic")
|
||||
@@ -660,13 +638,8 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
position_rendered = self._set_position_template(
|
||||
position_ranged, variables=variables
|
||||
)
|
||||
|
||||
await self.async_publish(
|
||||
self._config[CONF_SET_POSITION_TOPIC],
|
||||
position_rendered,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_SET_POSITION_TOPIC], position_rendered
|
||||
)
|
||||
if self._optimistic:
|
||||
self._update_state(
|
||||
|
@@ -3,10 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import datetime as dt
|
||||
from functools import wraps
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -21,34 +19,6 @@ from .models import DATA_MQTT, MessageCallbackType, PublishPayloadType
|
||||
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
|
||||
class TimestampedPublishMessage:
|
||||
"""MQTT Message."""
|
||||
|
@@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -155,6 +155,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
@@ -476,10 +476,14 @@ async def async_start( # noqa: C901
|
||||
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 = [
|
||||
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 (
|
||||
f"{discovery_topic}/+/+/config",
|
||||
f"{discovery_topic}/+/+/+/config",
|
||||
|
@@ -17,7 +17,7 @@ from homeassistant.components.event import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -200,6 +200,7 @@ class MqttEvent(MqttEntity, EventEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
|
@@ -27,7 +27,7 @@ from homeassistant.const import (
|
||||
CONF_PAYLOAD_ON,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.template import Template
|
||||
@@ -45,7 +45,6 @@ from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_STATE_VALUE_TEMPLATE,
|
||||
PAYLOAD_NONE,
|
||||
@@ -447,6 +446,7 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
return has_topic
|
||||
|
||||
@@ -496,12 +496,8 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"])
|
||||
await self.async_publish(
|
||||
self._topic[CONF_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
if percentage:
|
||||
await self.async_set_percentage(percentage)
|
||||
@@ -517,12 +513,8 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"])
|
||||
await self.async_publish(
|
||||
self._topic[CONF_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
if self._optimistic:
|
||||
self._attr_is_on = False
|
||||
@@ -537,14 +529,9 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
percentage_to_ranged_value(self._speed_range, percentage)
|
||||
)
|
||||
mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload)
|
||||
await self.async_publish(
|
||||
self._topic[CONF_PERCENTAGE_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_PERCENTAGE_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
|
||||
if self._optimistic_percentage:
|
||||
self._attr_percentage = percentage
|
||||
self.async_write_ha_state()
|
||||
@@ -555,15 +542,9 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode)
|
||||
|
||||
await self.async_publish(
|
||||
self._topic[CONF_PRESET_MODE_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_PRESET_MODE_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
|
||||
if self._optimistic_preset_mode:
|
||||
self._attr_preset_mode = preset_mode
|
||||
self.async_write_ha_state()
|
||||
@@ -581,15 +562,9 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
mqtt_payload = self._command_templates[ATTR_OSCILLATING](
|
||||
self._payload["OSCILLATE_OFF_PAYLOAD"]
|
||||
)
|
||||
|
||||
await self.async_publish(
|
||||
self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_OSCILLATION_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
|
||||
if self._optimistic_oscillation:
|
||||
self._attr_oscillating = oscillating
|
||||
self.async_write_ha_state()
|
||||
@@ -600,15 +575,9 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt_payload = self._command_templates[ATTR_DIRECTION](direction)
|
||||
|
||||
await self.async_publish(
|
||||
self._topic[CONF_DIRECTION_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_DIRECTION_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
|
||||
if self._optimistic_direction:
|
||||
self._attr_current_direction = direction
|
||||
self.async_write_ha_state()
|
||||
|
@@ -30,7 +30,7 @@ from homeassistant.const import (
|
||||
CONF_PAYLOAD_ON,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.template import Template
|
||||
@@ -47,7 +47,6 @@ from .const import (
|
||||
CONF_CURRENT_HUMIDITY_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_STATE_VALUE_TEMPLATE,
|
||||
PAYLOAD_NONE,
|
||||
@@ -293,6 +292,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": qos,
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
@callback
|
||||
@@ -455,12 +455,8 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"])
|
||||
await self.async_publish(
|
||||
self._topic[CONF_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
if self._optimistic:
|
||||
self._attr_is_on = True
|
||||
@@ -472,12 +468,8 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"])
|
||||
await self.async_publish(
|
||||
self._topic[CONF_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
if self._optimistic:
|
||||
self._attr_is_on = False
|
||||
@@ -489,14 +481,9 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt_payload = self._command_templates[ATTR_HUMIDITY](humidity)
|
||||
await self.async_publish(
|
||||
self._topic[CONF_TARGET_HUMIDITY_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_TARGET_HUMIDITY_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
|
||||
if self._optimistic_target_humidity:
|
||||
self._attr_target_humidity = humidity
|
||||
self.async_write_ha_state()
|
||||
@@ -511,15 +498,9 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
|
||||
return
|
||||
|
||||
mqtt_payload = self._command_templates[ATTR_MODE](mode)
|
||||
|
||||
await self.async_publish(
|
||||
self._topic[CONF_MODE_COMMAND_TOPIC],
|
||||
mqtt_payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_MODE_COMMAND_TOPIC], mqtt_payload
|
||||
)
|
||||
|
||||
if self._optimistic_mode:
|
||||
self._attr_mode = mode
|
||||
self.async_write_ha_state()
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.components import image
|
||||
from homeassistant.components.image import DEFAULT_CONTENT_TYPE, ImageEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
@@ -202,6 +202,7 @@ class MqttImage(MqttEntity, ImageEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": encoding,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
return has_topic
|
||||
|
||||
|
@@ -17,7 +17,7 @@ from homeassistant.components.lawn_mower import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -192,6 +192,7 @@ class MqttLawnMower(MqttEntity, LawnMowerEntity, RestoreEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"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:
|
||||
self._attr_activity = activity
|
||||
self.async_write_ha_state()
|
||||
|
||||
await self.async_publish(
|
||||
self._command_topics[option],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._command_topics[option], payload)
|
||||
|
||||
async def async_start_mowing(self) -> None:
|
||||
"""Start or resume mowing."""
|
||||
|
@@ -37,7 +37,7 @@ from homeassistant.const import (
|
||||
CONF_PAYLOAD_ON,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HassJobType, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -49,7 +49,6 @@ from ..const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_STATE_VALUE_TEMPLATE,
|
||||
PAYLOAD_NONE,
|
||||
@@ -580,6 +579,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
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:
|
||||
"""Publish an MQTT message."""
|
||||
await self.async_publish(
|
||||
str(self._topic[topic]),
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(str(self._topic[topic]), payload)
|
||||
|
||||
def scale_rgbx(
|
||||
color: tuple[int, ...],
|
||||
@@ -875,12 +869,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
await self.async_publish(
|
||||
str(self._topic[CONF_COMMAND_TOPIC]),
|
||||
self._payload["off"],
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
str(self._topic[CONF_COMMAND_TOPIC]), self._payload["off"]
|
||||
)
|
||||
|
||||
if self._optimistic:
|
||||
|
@@ -47,7 +47,7 @@ from homeassistant.const import (
|
||||
CONF_XY,
|
||||
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
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
@@ -522,6 +522,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"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]
|
||||
should_update = True
|
||||
|
||||
await self.async_publish(
|
||||
str(self._topic[CONF_COMMAND_TOPIC]),
|
||||
json_dumps(message),
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
str(self._topic[CONF_COMMAND_TOPIC]), json_dumps(message)
|
||||
)
|
||||
|
||||
if self._optimistic:
|
||||
@@ -762,12 +759,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
|
||||
self._set_flash_and_transition(message, **kwargs)
|
||||
|
||||
await self.async_publish(
|
||||
str(self._topic[CONF_COMMAND_TOPIC]),
|
||||
json_dumps(message),
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
str(self._topic[CONF_COMMAND_TOPIC]), json_dumps(message)
|
||||
)
|
||||
|
||||
if self._optimistic:
|
||||
|
@@ -29,7 +29,7 @@ from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HassJobType, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
@@ -41,7 +41,6 @@ from ..const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
@@ -282,6 +281,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"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:
|
||||
values["transition"] = kwargs[ATTR_TRANSITION]
|
||||
|
||||
await self.async_publish(
|
||||
await self.async_publish_with_config(
|
||||
str(self._topics[CONF_COMMAND_TOPIC]),
|
||||
self._command_templates[CONF_COMMAND_ON_TEMPLATE](None, values),
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
|
||||
if self._optimistic:
|
||||
@@ -387,12 +384,9 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
values["transition"] = kwargs[ATTR_TRANSITION]
|
||||
|
||||
await self.async_publish(
|
||||
await self.async_publish_with_config(
|
||||
str(self._topics[CONF_COMMAND_TOPIC]),
|
||||
self._command_templates[CONF_COMMAND_OFF_TEMPLATE](None, values),
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
|
||||
if self._optimistic:
|
||||
|
@@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
CONF_OPTIMISTIC,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
@@ -32,7 +32,6 @@ from .const import (
|
||||
CONF_ENCODING,
|
||||
CONF_PAYLOAD_RESET,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_OPEN,
|
||||
CONF_STATE_OPENING,
|
||||
CONF_STATE_TOPIC,
|
||||
@@ -232,6 +231,7 @@ class MqttLock(MqttEntity, LockEntity):
|
||||
"entity_id": self.entity_id,
|
||||
CONF_QOS: qos,
|
||||
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
|
||||
}
|
||||
payload = self._command_template(self._config[CONF_PAYLOAD_LOCK], tpl_vars)
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that the lock has changed state.
|
||||
self._attr_is_locked = True
|
||||
@@ -275,13 +269,7 @@ class MqttLock(MqttEntity, LockEntity):
|
||||
ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None
|
||||
}
|
||||
payload = self._command_template(self._config[CONF_PAYLOAD_UNLOCK], tpl_vars)
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that the lock has changed state.
|
||||
self._attr_is_locked = False
|
||||
@@ -296,13 +284,7 @@ class MqttLock(MqttEntity, LockEntity):
|
||||
ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None
|
||||
}
|
||||
payload = self._command_template(self._config[CONF_PAYLOAD_OPEN], tpl_vars)
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that the lock unlocks when opened.
|
||||
self._attr_is_open = True
|
||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable, Coroutine
|
||||
import functools
|
||||
from functools import partial, wraps
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol, cast, final
|
||||
|
||||
@@ -30,7 +30,7 @@ from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
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.device_registry import (
|
||||
DeviceEntry,
|
||||
@@ -83,6 +83,7 @@ from .const import (
|
||||
CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_SCHEMA,
|
||||
CONF_SERIAL_NUMBER,
|
||||
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):
|
||||
"""Mixin used for platforms that support JSON attributes."""
|
||||
|
||||
@@ -426,9 +388,10 @@ class MqttAttributesMixin(Entity):
|
||||
|
||||
def _attributes_prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
self._attr_tpl = MqttValueTemplate(
|
||||
self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE), entity=self
|
||||
).async_render_with_possible_json_value
|
||||
if template := self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE):
|
||||
self._attr_tpl = MqttValueTemplate(
|
||||
template, entity=self
|
||||
).async_render_with_possible_json_value
|
||||
self._attributes_sub_state = async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._attributes_sub_state,
|
||||
@@ -443,6 +406,7 @@ class MqttAttributesMixin(Entity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._attributes_config.get(CONF_QOS),
|
||||
"encoding": self._attributes_config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -461,9 +425,9 @@ class MqttAttributesMixin(Entity):
|
||||
@callback
|
||||
def _attributes_message_received(self, msg: ReceiveMessage) -> None:
|
||||
"""Update extra state attributes."""
|
||||
if TYPE_CHECKING:
|
||||
assert self._attr_tpl is not None
|
||||
payload = self._attr_tpl(msg.payload)
|
||||
payload = (
|
||||
self._attr_tpl(msg.payload) if self._attr_tpl is not None else msg.payload
|
||||
)
|
||||
try:
|
||||
json_dict = json_loads(payload) if isinstance(payload, str) else None
|
||||
except ValueError:
|
||||
@@ -557,6 +521,7 @@ class MqttAvailabilityMixin(Entity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._avail_config[CONF_QOS],
|
||||
"encoding": self._avail_config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
for topic in self._avail_topics
|
||||
}
|
||||
@@ -1192,6 +1157,18 @@ class MqttEntity(
|
||||
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
|
||||
@abstractmethod
|
||||
def config_schema() -> vol.Schema:
|
||||
|
@@ -14,13 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
)
|
||||
from .const import CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_RETAIN
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MqttCommandTemplate
|
||||
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:
|
||||
"""Send a message."""
|
||||
payload = self._command_template(message)
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
|
@@ -26,7 +26,7 @@ from homeassistant.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
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.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -39,7 +39,6 @@ from .const import (
|
||||
CONF_ENCODING,
|
||||
CONF_PAYLOAD_RESET,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
)
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
@@ -214,6 +213,7 @@ class MqttNumber(MqttEntity, RestoreNumber):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -238,11 +238,4 @@ class MqttNumber(MqttEntity, RestoreNumber):
|
||||
if self._attr_assumed_state:
|
||||
self._attr_native_value = current_number
|
||||
self.async_write_ha_state()
|
||||
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
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 .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
from .util import valid_publish_topic
|
||||
@@ -83,10 +83,6 @@ class MqttScene(
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
self._config[CONF_PAYLOAD_ON],
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON]
|
||||
)
|
||||
|
@@ -12,7 +12,7 @@ from homeassistant.components import select
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -25,7 +25,6 @@ from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
)
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
@@ -154,6 +153,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"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:
|
||||
self._attr_current_option = option
|
||||
self.async_write_ha_state()
|
||||
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
|
@@ -31,7 +31,13 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
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
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
@@ -297,6 +303,7 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
|
@@ -28,7 +28,7 @@ from homeassistant.const import (
|
||||
CONF_PAYLOAD_OFF,
|
||||
CONF_PAYLOAD_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
@@ -43,7 +43,6 @@ from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_STATE_VALUE_TEMPLATE,
|
||||
PAYLOAD_EMPTY_JSON,
|
||||
@@ -282,6 +281,7 @@ class MqttSiren(MqttEntity, SirenEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -318,13 +318,7 @@ class MqttSiren(MqttEntity, SirenEntity):
|
||||
else:
|
||||
payload = json_dumps(template_variables)
|
||||
if payload and str(payload) != PAYLOAD_NONE:
|
||||
await self.async_publish(
|
||||
self._config[topic],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[topic], payload)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren on.
|
||||
|
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
|
||||
from . import debug_info
|
||||
from .client import async_subscribe_internal
|
||||
@@ -27,6 +27,7 @@ class EntitySubscription:
|
||||
qos: int = 0
|
||||
encoding: str = "utf-8"
|
||||
entity_id: str | None = None
|
||||
job_type: HassJobType | None = None
|
||||
|
||||
def resubscribe_if_necessary(
|
||||
self, hass: HomeAssistant, other: EntitySubscription | None
|
||||
@@ -62,7 +63,12 @@ class EntitySubscription:
|
||||
if not self.should_subscribe or not self.topic:
|
||||
return
|
||||
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:
|
||||
@@ -112,6 +118,7 @@ def async_prepare_subscribe_topics(
|
||||
hass=hass,
|
||||
should_subscribe=None,
|
||||
entity_id=value.get("entity_id", None),
|
||||
job_type=value.get("job_type", None),
|
||||
)
|
||||
# Get the current subscription state
|
||||
current = current_subscriptions.pop(key, None)
|
||||
|
@@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -33,7 +33,6 @@ from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
@@ -145,6 +144,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"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.
|
||||
"""
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
self._config[CONF_PAYLOAD_ON],
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON]
|
||||
)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that switch has changed state.
|
||||
@@ -178,12 +174,8 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
self._config[CONF_PAYLOAD_OFF],
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_OFF]
|
||||
)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that switch has changed state.
|
||||
|
@@ -11,7 +11,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import tag
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
@@ -142,28 +142,32 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdateMixin):
|
||||
update_device(self.hass, self._config_entry, config)
|
||||
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:
|
||||
"""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.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
"state_topic": {
|
||||
"topic": self._config[CONF_TOPIC],
|
||||
"msg_callback": tag_scanned,
|
||||
"msg_callback": self._async_tag_scanned,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE,
|
||||
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.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -32,7 +32,6 @@ from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
)
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
@@ -183,6 +182,7 @@ class MqttTextEntity(MqttEntity, TextEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
add_subscription(
|
||||
@@ -203,14 +203,7 @@ class MqttTextEntity(MqttEntity, TextEntity):
|
||||
async def async_set_value(self, value: str) -> None:
|
||||
"""Change the text."""
|
||||
payload = self._command_template(value)
|
||||
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
if self._optimistic:
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
|
@@ -10,7 +10,13 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
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.template import Template
|
||||
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
|
||||
)
|
||||
|
||||
return await mqtt.async_subscribe(
|
||||
hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos
|
||||
return mqtt.async_subscribe_internal(
|
||||
hass,
|
||||
topic,
|
||||
mqtt_automation_listener,
|
||||
encoding=encoding,
|
||||
qos=qos,
|
||||
job_type=HassJobType.Callback,
|
||||
)
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.components.update import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -229,6 +229,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
add_subscription(
|
||||
@@ -264,14 +265,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
|
||||
) -> None:
|
||||
"""Update the current value."""
|
||||
payload = self._config[CONF_PAYLOAD_INSTALL]
|
||||
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> UpdateEntityFeature:
|
||||
|
@@ -31,7 +31,7 @@ from homeassistant.const import (
|
||||
STATE_IDLE,
|
||||
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
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
@@ -346,6 +346,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
@@ -359,13 +360,8 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
"""Publish a command."""
|
||||
if self._command_topic is None:
|
||||
return
|
||||
|
||||
await self.async_publish(
|
||||
self._command_topic,
|
||||
self._payloads[_FEATURE_PAYLOADS[feature]],
|
||||
qos=self._config[CONF_QOS],
|
||||
retain=self._config[CONF_RETAIN],
|
||||
encoding=self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._command_topic, self._payloads[_FEATURE_PAYLOADS[feature]]
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -401,13 +397,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
or (fan_speed not in self.fan_speed_list)
|
||||
):
|
||||
return
|
||||
await self.async_publish(
|
||||
self._set_fan_speed_topic,
|
||||
fan_speed,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._set_fan_speed_topic, fan_speed)
|
||||
|
||||
async def async_send_command(
|
||||
self,
|
||||
@@ -427,10 +417,4 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
payload = json_dumps(message)
|
||||
else:
|
||||
payload = command
|
||||
await self.async_publish(
|
||||
self._send_command_topic,
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._send_command_topic, payload)
|
||||
|
@@ -26,7 +26,7 @@ from homeassistant.const import (
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -357,6 +357,7 @@ class MqttValve(MqttEntity, ValveEntity):
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
@@ -375,13 +376,7 @@ class MqttValve(MqttEntity, ValveEntity):
|
||||
payload = self._command_template(
|
||||
self._config.get(CONF_PAYLOAD_OPEN, DEFAULT_PAYLOAD_OPEN)
|
||||
)
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that valve has changed state.
|
||||
self._update_state(STATE_OPEN)
|
||||
@@ -395,13 +390,7 @@ class MqttValve(MqttEntity, ValveEntity):
|
||||
payload = self._command_template(
|
||||
self._config.get(CONF_PAYLOAD_CLOSE, DEFAULT_PAYLOAD_CLOSE)
|
||||
)
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that valve has changed state.
|
||||
self._update_state(STATE_CLOSED)
|
||||
@@ -413,13 +402,7 @@ class MqttValve(MqttEntity, ValveEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
payload = self._command_template(self._config[CONF_PAYLOAD_STOP])
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
payload,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
await self.async_publish_with_config(self._config[CONF_COMMAND_TOPIC], payload)
|
||||
|
||||
async def async_set_valve_position(self, position: int) -> None:
|
||||
"""Move the valve to a specific position."""
|
||||
@@ -433,13 +416,8 @@ class MqttValve(MqttEntity, ValveEntity):
|
||||
"position_closed": self._config[CONF_POSITION_CLOSED],
|
||||
}
|
||||
rendered_position = self._command_template(scaled_position, variables=variables)
|
||||
|
||||
await self.async_publish(
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
rendered_position,
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
self._config[CONF_ENCODING],
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_COMMAND_TOPIC], rendered_position
|
||||
)
|
||||
if self._optimistic:
|
||||
self._update_state(
|
||||
|
@@ -609,6 +609,15 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity):
|
||||
)
|
||||
coro = self._async_run(variables, context)
|
||||
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
|
||||
return script_result.service_response if script_result else None
|
||||
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/subaru",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["stdiomask", "subarulink"],
|
||||
"requirements": ["subarulink==0.7.9"]
|
||||
"requirements": ["subarulink==0.7.11"]
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
import subarulink.const as sc
|
||||
|
||||
@@ -23,11 +23,7 @@ from homeassistant.helpers.update_coordinator import (
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter
|
||||
from homeassistant.util.unit_system import (
|
||||
LENGTH_UNITS,
|
||||
PRESSURE_UNITS,
|
||||
US_CUSTOMARY_SYSTEM,
|
||||
)
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from . import get_device_info
|
||||
from .const import (
|
||||
@@ -58,7 +54,7 @@ SAFETY_SENSORS = [
|
||||
key=sc.ODOMETER,
|
||||
translation_key="odometer",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
native_unit_of_measurement=UnitOfLength.MILES,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
]
|
||||
@@ -68,42 +64,42 @@ API_GEN_2_SENSORS = [
|
||||
SensorEntityDescription(
|
||||
key=sc.AVG_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,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=sc.DIST_TO_EMPTY,
|
||||
translation_key="range",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
native_unit_of_measurement=UnitOfLength.MILES,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=sc.TIRE_PRESSURE_FL,
|
||||
translation_key="tire_pressure_front_left",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=sc.TIRE_PRESSURE_FR,
|
||||
translation_key="tire_pressure_front_right",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=sc.TIRE_PRESSURE_RL,
|
||||
translation_key="tire_pressure_rear_left",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=sc.TIRE_PRESSURE_RR,
|
||||
translation_key="tire_pressure_rear_right",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
]
|
||||
@@ -207,30 +203,13 @@ class SubaruSensor(
|
||||
@property
|
||||
def native_value(self) -> int | float | None:
|
||||
"""Return the state of the sensor."""
|
||||
vehicle_data = self.coordinator.data[self.vin]
|
||||
current_value = vehicle_data[VEHICLE_STATUS].get(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,
|
||||
)
|
||||
current_value = self.coordinator.data[self.vin][VEHICLE_STATUS].get(
|
||||
self.entity_description.key
|
||||
)
|
||||
|
||||
if (
|
||||
unit
|
||||
in [
|
||||
FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
|
||||
FUEL_CONSUMPTION_MILES_PER_GALLON,
|
||||
]
|
||||
and unit_system == US_CUSTOMARY_SYSTEM
|
||||
self.entity_description.key == sc.AVG_FUEL_CONSUMPTION
|
||||
and self.hass.config.units == METRIC_SYSTEM
|
||||
):
|
||||
return round((100.0 * L_PER_GAL) / (KM_PER_MI * current_value), 1)
|
||||
|
||||
@@ -239,23 +218,12 @@ class SubaruSensor(
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit_of_measurement of the device."""
|
||||
unit = self.entity_description.native_unit_of_measurement
|
||||
|
||||
if unit in LENGTH_UNITS:
|
||||
return self.hass.config.units.length_unit
|
||||
|
||||
if unit in PRESSURE_UNITS:
|
||||
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
|
||||
if (
|
||||
self.entity_description.key == sc.AVG_FUEL_CONSUMPTION
|
||||
and self.hass.config.units == METRIC_SYSTEM
|
||||
):
|
||||
return FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
@@ -2,11 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from aioswitcher.api import (
|
||||
DeviceState,
|
||||
SwitcherApi,
|
||||
SwitcherBaseResponse,
|
||||
SwitcherType2Api,
|
||||
ThermostatSwing,
|
||||
@@ -34,7 +36,10 @@ from .utils import get_breeze_remote_manager
|
||||
class SwitcherThermostatButtonEntityDescription(ButtonEntityDescription):
|
||||
"""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]
|
||||
|
||||
|
||||
@@ -85,9 +90,10 @@ async def async_setup_entry(
|
||||
|
||||
async def async_add_buttons(coordinator: SwitcherDataUpdateCoordinator) -> None:
|
||||
"""Get remote and add button from Switcher device."""
|
||||
data = cast(SwitcherBreezeRemote, coordinator.data)
|
||||
if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT:
|
||||
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(
|
||||
SwitcherThermostatButtonEntity(coordinator, description, remote)
|
||||
@@ -126,7 +132,7 @@ class SwitcherThermostatButtonEntity(
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
response: SwitcherBaseResponse = None
|
||||
response: SwitcherBaseResponse | None = None
|
||||
error = None
|
||||
|
||||
try:
|
||||
|
@@ -9,6 +9,7 @@ from aioswitcher.api.remotes import SwitcherBreezeRemote
|
||||
from aioswitcher.device import (
|
||||
DeviceCategory,
|
||||
DeviceState,
|
||||
SwitcherThermostat,
|
||||
ThermostatFanLevel,
|
||||
ThermostatMode,
|
||||
ThermostatSwing,
|
||||
@@ -68,9 +69,10 @@ async def async_setup_entry(
|
||||
|
||||
async def async_add_climate(coordinator: SwitcherDataUpdateCoordinator) -> None:
|
||||
"""Get remote and add climate from Switcher device."""
|
||||
data = cast(SwitcherThermostat, coordinator.data)
|
||||
if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT:
|
||||
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)])
|
||||
|
||||
@@ -133,13 +135,13 @@ class SwitcherClimateEntity(
|
||||
|
||||
def _update_data(self, force_update: bool = False) -> None:
|
||||
"""Update data from device."""
|
||||
data = self.coordinator.data
|
||||
data = cast(SwitcherThermostat, self.coordinator.data)
|
||||
features = self._remote.modes_features[data.mode]
|
||||
|
||||
if data.target_temperature == 0 and not force_update:
|
||||
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_hvac_mode = HVACMode.OFF
|
||||
@@ -162,7 +164,7 @@ class SwitcherClimateEntity(
|
||||
|
||||
async def _async_control_breeze_device(self, **kwargs: Any) -> None:
|
||||
"""Call Switcher Control Breeze API."""
|
||||
response: SwitcherBaseResponse = None
|
||||
response: SwitcherBaseResponse | None = None
|
||||
error = None
|
||||
|
||||
try:
|
||||
@@ -185,9 +187,8 @@ class SwitcherClimateEntity(
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if not self._remote.modes_features[self.coordinator.data.mode][
|
||||
"temperature_control"
|
||||
]:
|
||||
data = cast(SwitcherThermostat, self.coordinator.data)
|
||||
if not self._remote.modes_features[data.mode]["temperature_control"]:
|
||||
raise HomeAssistantError(
|
||||
"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:
|
||||
"""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")
|
||||
|
||||
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:
|
||||
"""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")
|
||||
|
||||
if swing_mode == SWING_VERTICAL:
|
||||
|
@@ -45,17 +45,17 @@ class SwitcherDataUpdateCoordinator(
|
||||
@property
|
||||
def model(self) -> str:
|
||||
"""Switcher device model."""
|
||||
return self.data.device_type.value # type: ignore[no-any-return]
|
||||
return self.data.device_type.value
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
"""Switcher device id."""
|
||||
return self.data.device_id # type: ignore[no-any-return]
|
||||
return self.data.device_id
|
||||
|
||||
@property
|
||||
def mac_address(self) -> str:
|
||||
"""Switcher device mac address."""
|
||||
return self.data.mac_address # type: ignore[no-any-return]
|
||||
return self.data.mac_address
|
||||
|
||||
@callback
|
||||
def async_setup(self) -> None:
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api
|
||||
from aioswitcher.device import DeviceCategory, ShutterDirection, SwitcherShutter
|
||||
@@ -84,7 +84,7 @@ class SwitcherCoverEntity(
|
||||
|
||||
def _update_data(self) -> None:
|
||||
"""Update data from device."""
|
||||
data: SwitcherShutter = self.coordinator.data
|
||||
data = cast(SwitcherShutter, self.coordinator.data)
|
||||
self._attr_current_cover_position = data.position
|
||||
self._attr_is_closed = data.position == 0
|
||||
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:
|
||||
"""Call Switcher API."""
|
||||
_LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args)
|
||||
response: SwitcherBaseResponse = None
|
||||
response: SwitcherBaseResponse | None = None
|
||||
error = None
|
||||
|
||||
try:
|
||||
|
@@ -7,6 +7,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioswitcher"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioswitcher==3.4.1"],
|
||||
"requirements": ["aioswitcher==3.4.3"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -111,7 +111,7 @@ class SwitcherBaseSwitchEntity(
|
||||
_LOGGER.debug(
|
||||
"Calling api for %s, api: '%s', args: %s", self.coordinator.name, api, args
|
||||
)
|
||||
response: SwitcherBaseResponse = None
|
||||
response: SwitcherBaseResponse | None = None
|
||||
error = None
|
||||
|
||||
try:
|
||||
|
@@ -28,13 +28,17 @@ from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
|
||||
|
||||
PLATFORMS: Final = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.LOCK,
|
||||
Platform.MEDIA_PLAYER,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.UPDATE,
|
||||
]
|
||||
|
||||
|
||||
|
85
homeassistant/components/teslemetry/button.py
Normal file
85
homeassistant/components/teslemetry/button.py
Normal 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))
|
@@ -60,6 +60,12 @@ class TeslemetryEntity(
|
||||
"""Return a specific value from coordinator data."""
|
||||
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
|
||||
def is_none(self) -> bool:
|
||||
"""Return if the value is a literal None."""
|
||||
|
@@ -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": {
|
||||
"driver_temp": {
|
||||
"state_attributes": {
|
||||
|
149
homeassistant/components/teslemetry/media_player.py
Normal file
149
homeassistant/components/teslemetry/media_player.py
Normal 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())
|
201
homeassistant/components/teslemetry/number.py
Normal file
201
homeassistant/components/teslemetry/number.py
Normal 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()
|
@@ -96,6 +96,26 @@
|
||||
"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": {
|
||||
"driver_temp": {
|
||||
"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": {
|
||||
"charge_state_charge_port_door_open": {
|
||||
"name": "Charge port door"
|
||||
@@ -415,6 +454,11 @@
|
||||
"vehicle_state_valet_mode": {
|
||||
"name": "Valet mode"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"vehicle_state_software_update_status": {
|
||||
"name": "[%key:component::update::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
105
homeassistant/components/teslemetry/update.py
Normal file
105
homeassistant/components/teslemetry/update.py
Normal 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()
|
@@ -157,7 +157,7 @@ SCRIPT_DEBUG_CONTINUE_STOP: SignalTypeFormat[Literal["continue", "stop"]] = (
|
||||
)
|
||||
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):
|
||||
@@ -452,7 +452,7 @@ class _ScriptRun:
|
||||
if (script_stack := script_stack_cv.get()) is None:
|
||||
script_stack = []
|
||||
script_stack_cv.set(script_stack)
|
||||
script_stack.append(id(self._script))
|
||||
script_stack.append(self._script.unique_id)
|
||||
response = None
|
||||
|
||||
try:
|
||||
@@ -1401,6 +1401,7 @@ class Script:
|
||||
self.sequence = sequence
|
||||
template.attach(hass, self.sequence)
|
||||
self.name = name
|
||||
self.unique_id = f"{domain}.{name}-{id(self)}"
|
||||
self.domain = domain
|
||||
self.running_description = running_description or f"{domain} script"
|
||||
self._change_listener = change_listener
|
||||
@@ -1723,10 +1724,21 @@ class Script:
|
||||
if (
|
||||
self.script_mode in (SCRIPT_MODE_RESTART, SCRIPT_MODE_QUEUED)
|
||||
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")
|
||||
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
|
||||
|
||||
if self.script_mode != SCRIPT_MODE_QUEUED:
|
||||
|
@@ -327,7 +327,33 @@ def _false(arg: str) -> bool:
|
||||
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:
|
||||
@@ -588,31 +614,7 @@ class Template:
|
||||
def _parse_result(self, render_result: str) -> Any:
|
||||
"""Parse the result."""
|
||||
try:
|
||||
result = _cached_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 _cached_parse_result(render_result)
|
||||
except (ValueError, TypeError, SyntaxError, MemoryError):
|
||||
pass
|
||||
|
||||
|
@@ -371,7 +371,7 @@ aiosolaredge==0.2.0
|
||||
aiosteamist==0.3.2
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==3.4.1
|
||||
aioswitcher==3.4.3
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.5.1
|
||||
@@ -1646,7 +1646,7 @@ py-nightscout==1.2.2
|
||||
py-schluter==0.1.7
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
py-sucks==0.9.9
|
||||
py-sucks==0.9.10
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.4.2
|
||||
@@ -2650,7 +2650,7 @@ streamlabswater==1.0.1
|
||||
stringcase==1.2.0
|
||||
|
||||
# homeassistant.components.subaru
|
||||
subarulink==0.7.9
|
||||
subarulink==0.7.11
|
||||
|
||||
# homeassistant.components.solarlog
|
||||
sunwatcher==0.2.1
|
||||
|
@@ -344,7 +344,7 @@ aiosolaredge==0.2.0
|
||||
aiosteamist==0.3.2
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==3.4.1
|
||||
aioswitcher==3.4.3
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.5.1
|
||||
@@ -1308,7 +1308,7 @@ py-nextbusnext==1.0.2
|
||||
py-nightscout==1.2.2
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
py-sucks==0.9.9
|
||||
py-sucks==0.9.10
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.4.2
|
||||
@@ -2063,7 +2063,7 @@ streamlabswater==1.0.1
|
||||
stringcase==1.2.0
|
||||
|
||||
# homeassistant.components.subaru
|
||||
subarulink==0.7.9
|
||||
subarulink==0.7.11
|
||||
|
||||
# homeassistant.components.solarlog
|
||||
sunwatcher==0.2.1
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest import mock
|
||||
from unittest.mock import Mock, call, patch
|
||||
from unittest.mock import ANY, Mock, call, patch
|
||||
|
||||
import axis as axislib
|
||||
import pytest
|
||||
@@ -90,7 +90,7 @@ async def test_device_support_mqtt(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry
|
||||
) -> None:
|
||||
"""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
|
||||
|
||||
topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0"
|
||||
|
@@ -71,7 +71,6 @@ async def test_buttons(
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_press_action.assert_called_once()
|
||||
|
||||
button = hass.states.get(entity_id)
|
||||
@@ -105,7 +104,6 @@ async def test_wol_button(
|
||||
{ATTR_ENTITY_ID: "button.printer_wake_on_lan"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_press_action.assert_called_once_with("AA:BB:CC:00:11:22")
|
||||
|
||||
button = hass.states.get("button.printer_wake_on_lan")
|
||||
|
@@ -145,7 +145,6 @@ async def test_user(
|
||||
== DEFAULT_CONSIDER_HOME.total_seconds()
|
||||
)
|
||||
assert not result["result"].unique_id
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_setup_entry.called
|
||||
|
||||
@@ -764,14 +763,12 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
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["flow_id"],
|
||||
user_input={
|
||||
CONF_CONSIDER_HOME: 37,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
|
@@ -56,7 +56,6 @@ async def test_options_reload(
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_CONSIDER_HOME: 60},
|
||||
|
@@ -971,6 +971,36 @@ async def test_timers_not_supported(hass: HomeAssistant) -> None:
|
||||
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:
|
||||
"""Test getting the status of named timers."""
|
||||
|
@@ -27,7 +27,7 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HassJobType, HomeAssistant
|
||||
from homeassistant.generated.mqtt import MQTT
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
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 mqtt_mock.async_subscribe.call_count == len(topics) + 2 + DISCOVERY_COUNT
|
||||
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()
|
||||
|
||||
entity_registry.async_update_entity(
|
||||
@@ -1203,7 +1205,9 @@ async def help_test_entity_id_update_subscriptions(
|
||||
state = hass.states.get(f"{domain}.milk")
|
||||
assert state is not None
|
||||
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(
|
||||
|
@@ -154,7 +154,7 @@ async def test_qos_encoding_default(
|
||||
{"test_topic1": {"topic": "test-topic1", "msg_callback": msg_callback}},
|
||||
)
|
||||
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(
|
||||
@@ -183,7 +183,7 @@ async def test_qos_encoding_custom(
|
||||
},
|
||||
)
|
||||
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(
|
||||
|
@@ -6,7 +6,7 @@ import pytest
|
||||
|
||||
from homeassistant.components import automation
|
||||
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 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:
|
||||
@@ -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
|
||||
)
|
||||
|
@@ -66,7 +66,7 @@ async def test_subscribe(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# 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(
|
||||
|
@@ -128,7 +128,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None:
|
||||
user_input={CONF_API_KEY: "newkey"},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert entry.data[CONF_API_KEY] == "newkey"
|
||||
|
@@ -199,8 +199,6 @@ async def test_disable_service_call(hass: HomeAssistant) -> None:
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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 await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
|
@@ -1741,3 +1741,46 @@ async def test_responses_no_response(hass: HomeAssistant) -> 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
|
||||
|
@@ -62,19 +62,13 @@ MOCK_DATETIME = datetime.fromtimestamp(1595560000, UTC)
|
||||
|
||||
VEHICLE_STATUS_EV = {
|
||||
VEHICLE_STATUS: {
|
||||
"AVG_FUEL_CONSUMPTION": 2.3,
|
||||
"DISTANCE_TO_EMPTY_FUEL": 707,
|
||||
"DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
|
||||
"AVG_FUEL_CONSUMPTION": 51.1,
|
||||
"DISTANCE_TO_EMPTY_FUEL": 170,
|
||||
"DOOR_BOOT_POSITION": "CLOSED",
|
||||
"DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_ENGINE_HOOD_POSITION": "CLOSED",
|
||||
"DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_FRONT_LEFT_POSITION": "CLOSED",
|
||||
"DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_FRONT_RIGHT_POSITION": "CLOSED",
|
||||
"DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_REAR_LEFT_POSITION": "CLOSED",
|
||||
"DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_REAR_RIGHT_POSITION": "CLOSED",
|
||||
"EV_CHARGER_STATE_TYPE": "CHARGING",
|
||||
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
|
||||
@@ -85,37 +79,12 @@ VEHICLE_STATUS_EV = {
|
||||
"EV_STATE_OF_CHARGE_PERCENT": 20,
|
||||
"EV_TIME_TO_FULLY_CHARGED_UTC": MOCK_DATETIME,
|
||||
"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,
|
||||
"TRANSMISSION_MODE": "UNKNOWN",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": 0,
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": 2550,
|
||||
"TYRE_PRESSURE_REAR_LEFT": 2450,
|
||||
"TYRE_PRESSURE_FRONT_LEFT": 0.0,
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": 31.9,
|
||||
"TYRE_PRESSURE_REAR_LEFT": 32.6,
|
||||
"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",
|
||||
"WINDOW_BACK_STATUS": "UNKNOWN",
|
||||
"WINDOW_FRONT_LEFT_STATUS": "VENTED",
|
||||
@@ -123,7 +92,6 @@ VEHICLE_STATUS_EV = {
|
||||
"WINDOW_REAR_LEFT_STATUS": "UNKNOWN",
|
||||
"WINDOW_REAR_RIGHT_STATUS": "UNKNOWN",
|
||||
"WINDOW_SUNROOF_STATUS": "UNKNOWN",
|
||||
"HEADING": 170,
|
||||
"LATITUDE": 40.0,
|
||||
"LONGITUDE": -100.0,
|
||||
}
|
||||
@@ -132,53 +100,22 @@ VEHICLE_STATUS_EV = {
|
||||
|
||||
VEHICLE_STATUS_G3 = {
|
||||
VEHICLE_STATUS: {
|
||||
"AVG_FUEL_CONSUMPTION": 2.3,
|
||||
"DISTANCE_TO_EMPTY_FUEL": 707,
|
||||
"DOOR_BOOT_LOCK_STATUS": "UNKNOWN",
|
||||
"AVG_FUEL_CONSUMPTION": 51.1,
|
||||
"DISTANCE_TO_EMPTY_FUEL": 170,
|
||||
"DOOR_BOOT_POSITION": "CLOSED",
|
||||
"DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_ENGINE_HOOD_POSITION": "CLOSED",
|
||||
"DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_FRONT_LEFT_POSITION": "CLOSED",
|
||||
"DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_FRONT_RIGHT_POSITION": "CLOSED",
|
||||
"DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_REAR_LEFT_POSITION": "CLOSED",
|
||||
"DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN",
|
||||
"DOOR_REAR_RIGHT_POSITION": "CLOSED",
|
||||
"REMAINING_FUEL_PERCENT": 77,
|
||||
"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,
|
||||
"TRANSMISSION_MODE": "UNKNOWN",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": 2550,
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": 2550,
|
||||
"TYRE_PRESSURE_REAR_LEFT": 2450,
|
||||
"TYRE_PRESSURE_FRONT_LEFT": 0.0,
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": 31.9,
|
||||
"TYRE_PRESSURE_REAR_LEFT": 32.6,
|
||||
"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",
|
||||
"WINDOW_BACK_STATUS": "UNKNOWN",
|
||||
"WINDOW_FRONT_LEFT_STATUS": "VENTED",
|
||||
@@ -186,15 +123,14 @@ VEHICLE_STATUS_G3 = {
|
||||
"WINDOW_REAR_LEFT_STATUS": "UNKNOWN",
|
||||
"WINDOW_REAR_RIGHT_STATUS": "UNKNOWN",
|
||||
"WINDOW_SUNROOF_STATUS": "UNKNOWN",
|
||||
"HEADING": 170,
|
||||
"LATITUDE": 40.0,
|
||||
"LONGITUDE": -100.0,
|
||||
}
|
||||
}
|
||||
|
||||
EXPECTED_STATE_EV_IMPERIAL = {
|
||||
"AVG_FUEL_CONSUMPTION": "102.3",
|
||||
"DISTANCE_TO_EMPTY_FUEL": "439.3",
|
||||
"AVG_FUEL_CONSUMPTION": "51.1",
|
||||
"DISTANCE_TO_EMPTY_FUEL": "170",
|
||||
"EV_CHARGER_STATE_TYPE": "CHARGING",
|
||||
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
|
||||
"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_PERCENT": "20",
|
||||
"EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00",
|
||||
"ODOMETER": "766.8",
|
||||
"POSITION_HEADING_DEGREE": "150",
|
||||
"POSITION_SPEED_KMPH": "0",
|
||||
"POSITION_TIMESTAMP": 1595560000.0,
|
||||
"ODOMETER": "1234",
|
||||
"TIMESTAMP": 1595560000.0,
|
||||
"TRANSMISSION_MODE": "UNKNOWN",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": "0.0",
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": "37.0",
|
||||
"TYRE_PRESSURE_REAR_LEFT": "35.5",
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": "31.9",
|
||||
"TYRE_PRESSURE_REAR_LEFT": "32.6",
|
||||
"TYRE_PRESSURE_REAR_RIGHT": "unknown",
|
||||
"VEHICLE_STATE_TYPE": "IGNITION_OFF",
|
||||
"HEADING": 170,
|
||||
"LATITUDE": 40.0,
|
||||
"LONGITUDE": -100.0,
|
||||
}
|
||||
|
||||
EXPECTED_STATE_EV_METRIC = {
|
||||
"AVG_FUEL_CONSUMPTION": "2.3",
|
||||
"DISTANCE_TO_EMPTY_FUEL": "707",
|
||||
"AVG_FUEL_CONSUMPTION": "4.6",
|
||||
"DISTANCE_TO_EMPTY_FUEL": "274",
|
||||
"EV_CHARGER_STATE_TYPE": "CHARGING",
|
||||
"EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM",
|
||||
"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_STATE_OF_CHARGE_MODE": "EV_MODE",
|
||||
"EV_STATE_OF_CHARGE_PERCENT": "20",
|
||||
"EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00",
|
||||
"ODOMETER": "1234",
|
||||
"POSITION_HEADING_DEGREE": "150",
|
||||
"POSITION_SPEED_KMPH": "0",
|
||||
"POSITION_TIMESTAMP": 1595560000.0,
|
||||
"ODOMETER": "1986",
|
||||
"TIMESTAMP": 1595560000.0,
|
||||
"TRANSMISSION_MODE": "UNKNOWN",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": "0",
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": "2550",
|
||||
"TYRE_PRESSURE_REAR_LEFT": "2450",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": "0.0",
|
||||
"TYRE_PRESSURE_FRONT_RIGHT": "219.9",
|
||||
"TYRE_PRESSURE_REAR_LEFT": "224.8",
|
||||
"TYRE_PRESSURE_REAR_RIGHT": "unknown",
|
||||
"VEHICLE_STATE_TYPE": "IGNITION_OFF",
|
||||
"HEADING": 170,
|
||||
"LATITUDE": 40.0,
|
||||
"LONGITUDE": -100.0,
|
||||
}
|
||||
@@ -259,9 +187,6 @@ EXPECTED_STATE_EV_UNAVAILABLE = {
|
||||
"EV_STATE_OF_CHARGE_PERCENT": "unavailable",
|
||||
"EV_TIME_TO_FULLY_CHARGED_UTC": "unavailable",
|
||||
"ODOMETER": "unavailable",
|
||||
"POSITION_HEADING_DEGREE": "unavailable",
|
||||
"POSITION_SPEED_KMPH": "unavailable",
|
||||
"POSITION_TIMESTAMP": "unavailable",
|
||||
"TIMESTAMP": "unavailable",
|
||||
"TRANSMISSION_MODE": "unavailable",
|
||||
"TYRE_PRESSURE_FRONT_LEFT": "unavailable",
|
||||
@@ -269,7 +194,6 @@ EXPECTED_STATE_EV_UNAVAILABLE = {
|
||||
"TYRE_PRESSURE_REAR_LEFT": "unavailable",
|
||||
"TYRE_PRESSURE_REAR_RIGHT": "unavailable",
|
||||
"VEHICLE_STATE_TYPE": "unavailable",
|
||||
"HEADING": "unavailable",
|
||||
"LATITUDE": "unavailable",
|
||||
"LONGITUDE": "unavailable",
|
||||
}
|
||||
|
@@ -11,19 +11,13 @@
|
||||
'data': list([
|
||||
dict({
|
||||
'vehicle_status': dict({
|
||||
'AVG_FUEL_CONSUMPTION': 2.3,
|
||||
'DISTANCE_TO_EMPTY_FUEL': 707,
|
||||
'DOOR_BOOT_LOCK_STATUS': 'UNKNOWN',
|
||||
'AVG_FUEL_CONSUMPTION': 51.1,
|
||||
'DISTANCE_TO_EMPTY_FUEL': 170,
|
||||
'DOOR_BOOT_POSITION': 'CLOSED',
|
||||
'DOOR_ENGINE_HOOD_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_ENGINE_HOOD_POSITION': 'CLOSED',
|
||||
'DOOR_FRONT_LEFT_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_FRONT_LEFT_POSITION': 'CLOSED',
|
||||
'DOOR_FRONT_RIGHT_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_FRONT_RIGHT_POSITION': 'CLOSED',
|
||||
'DOOR_REAR_LEFT_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_REAR_LEFT_POSITION': 'CLOSED',
|
||||
'DOOR_REAR_RIGHT_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_REAR_RIGHT_POSITION': 'CLOSED',
|
||||
'EV_CHARGER_STATE_TYPE': 'CHARGING',
|
||||
'EV_CHARGE_SETTING_AMPERE_TYPE': 'MAXIMUM',
|
||||
@@ -33,41 +27,15 @@
|
||||
'EV_STATE_OF_CHARGE_MODE': 'EV_MODE',
|
||||
'EV_STATE_OF_CHARGE_PERCENT': 20,
|
||||
'EV_TIME_TO_FULLY_CHARGED_UTC': '2020-07-24T03:06:40+00:00',
|
||||
'HEADING': 170,
|
||||
'LATITUDE': '**REDACTED**',
|
||||
'LONGITUDE': '**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,
|
||||
'TRANSMISSION_MODE': 'UNKNOWN',
|
||||
'TYRE_PRESSURE_FRONT_LEFT': 0,
|
||||
'TYRE_PRESSURE_FRONT_RIGHT': 2550,
|
||||
'TYRE_PRESSURE_REAR_LEFT': 2450,
|
||||
'TYRE_PRESSURE_FRONT_LEFT': 0.0,
|
||||
'TYRE_PRESSURE_FRONT_RIGHT': 31.9,
|
||||
'TYRE_PRESSURE_REAR_LEFT': 32.6,
|
||||
'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',
|
||||
'WINDOW_BACK_STATUS': 'UNKNOWN',
|
||||
'WINDOW_FRONT_LEFT_STATUS': 'VENTED',
|
||||
@@ -94,19 +62,13 @@
|
||||
}),
|
||||
'data': dict({
|
||||
'vehicle_status': dict({
|
||||
'AVG_FUEL_CONSUMPTION': 2.3,
|
||||
'DISTANCE_TO_EMPTY_FUEL': 707,
|
||||
'DOOR_BOOT_LOCK_STATUS': 'UNKNOWN',
|
||||
'AVG_FUEL_CONSUMPTION': 51.1,
|
||||
'DISTANCE_TO_EMPTY_FUEL': 170,
|
||||
'DOOR_BOOT_POSITION': 'CLOSED',
|
||||
'DOOR_ENGINE_HOOD_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_ENGINE_HOOD_POSITION': 'CLOSED',
|
||||
'DOOR_FRONT_LEFT_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_FRONT_LEFT_POSITION': 'CLOSED',
|
||||
'DOOR_FRONT_RIGHT_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_FRONT_RIGHT_POSITION': 'CLOSED',
|
||||
'DOOR_REAR_LEFT_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_REAR_LEFT_POSITION': 'CLOSED',
|
||||
'DOOR_REAR_RIGHT_LOCK_STATUS': 'UNKNOWN',
|
||||
'DOOR_REAR_RIGHT_POSITION': 'CLOSED',
|
||||
'EV_CHARGER_STATE_TYPE': 'CHARGING',
|
||||
'EV_CHARGE_SETTING_AMPERE_TYPE': 'MAXIMUM',
|
||||
@@ -116,41 +78,15 @@
|
||||
'EV_STATE_OF_CHARGE_MODE': 'EV_MODE',
|
||||
'EV_STATE_OF_CHARGE_PERCENT': 20,
|
||||
'EV_TIME_TO_FULLY_CHARGED_UTC': '2020-07-24T03:06:40+00:00',
|
||||
'HEADING': 170,
|
||||
'LATITUDE': '**REDACTED**',
|
||||
'LONGITUDE': '**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,
|
||||
'TRANSMISSION_MODE': 'UNKNOWN',
|
||||
'TYRE_PRESSURE_FRONT_LEFT': 0,
|
||||
'TYRE_PRESSURE_FRONT_RIGHT': 2550,
|
||||
'TYRE_PRESSURE_REAR_LEFT': 2450,
|
||||
'TYRE_PRESSURE_FRONT_LEFT': 0.0,
|
||||
'TYRE_PRESSURE_FRONT_RIGHT': 31.9,
|
||||
'TYRE_PRESSURE_REAR_LEFT': 32.6,
|
||||
'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',
|
||||
'WINDOW_BACK_STATUS': 'UNKNOWN',
|
||||
'WINDOW_FRONT_LEFT_STATUS': 'VENTED',
|
||||
|
@@ -14,14 +14,11 @@ from homeassistant.components.subaru.sensor import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||
|
||||
from .api_responses import (
|
||||
EXPECTED_STATE_EV_IMPERIAL,
|
||||
EXPECTED_STATE_EV_METRIC,
|
||||
EXPECTED_STATE_EV_UNAVAILABLE,
|
||||
TEST_VIN_2_EV,
|
||||
VEHICLE_STATUS_EV,
|
||||
)
|
||||
from .conftest import (
|
||||
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:
|
||||
"""Test sensors supporting metric units."""
|
||||
_assert_data(hass, EXPECTED_STATE_EV_METRIC)
|
||||
|
@@ -693,7 +693,7 @@ async def help_test_entity_id_update_subscriptions(
|
||||
assert state is not None
|
||||
assert mqtt_mock.async_subscribe.call_count == len(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()
|
||||
|
||||
entity_reg.async_update_entity(
|
||||
@@ -707,7 +707,7 @@ async def help_test_entity_id_update_subscriptions(
|
||||
state = hass.states.get(f"{domain}.milk")
|
||||
assert state is not None
|
||||
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(
|
||||
|
@@ -30,7 +30,9 @@ async def test_subscribing_config_topic(
|
||||
discovery_topic = DEFAULT_PREFIX
|
||||
|
||||
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(
|
||||
|
@@ -26,7 +26,7 @@
|
||||
"storm_mode_capable": true,
|
||||
"flex_energy_request_capable": 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_solar_offset_view_enabled": false,
|
||||
"battery_solar_offset_view_enabled": true,
|
||||
|
@@ -204,17 +204,18 @@
|
||||
"is_user_present": false,
|
||||
"locked": false,
|
||||
"media_info": {
|
||||
"audio_volume": 2.6667,
|
||||
"a2dp_source_name": "Pixel 8 Pro",
|
||||
"audio_volume": 1.6667,
|
||||
"audio_volume_increment": 0.333333,
|
||||
"audio_volume_max": 10.333333,
|
||||
"media_playback_status": "Stopped",
|
||||
"now_playing_album": "",
|
||||
"now_playing_artist": "",
|
||||
"now_playing_duration": 0,
|
||||
"now_playing_elapsed": 0,
|
||||
"now_playing_source": "Spotify",
|
||||
"now_playing_station": "",
|
||||
"now_playing_title": ""
|
||||
"media_playback_status": "Playing",
|
||||
"now_playing_album": "Elon Musk",
|
||||
"now_playing_artist": "Walter Isaacson",
|
||||
"now_playing_duration": 651000,
|
||||
"now_playing_elapsed": 1000,
|
||||
"now_playing_source": "Audible",
|
||||
"now_playing_station": "Elon Musk",
|
||||
"now_playing_title": "Chapter 51: Cybertruck: Tesla, 2018–2019"
|
||||
},
|
||||
"media_state": {
|
||||
"remote_control_enabled": true
|
||||
@@ -236,11 +237,11 @@
|
||||
"service_mode": false,
|
||||
"service_mode_plus": false,
|
||||
"software_update": {
|
||||
"download_perc": 0,
|
||||
"download_perc": 100,
|
||||
"expected_duration_sec": 2700,
|
||||
"install_perc": 1,
|
||||
"status": "",
|
||||
"version": " "
|
||||
"status": "available",
|
||||
"version": "2024.12.0.0"
|
||||
},
|
||||
"speed_limit_mode": {
|
||||
"active": false,
|
||||
|
323
tests/components/teslemetry/snapshots/test_button.ambr
Normal file
323
tests/components/teslemetry/snapshots/test_button.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
@@ -62,7 +62,7 @@
|
||||
'components_grid_services_enabled': False,
|
||||
'components_load_meter': True,
|
||||
'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_show_grid_import_battery_source_cards': True,
|
||||
'components_solar': True,
|
||||
@@ -361,17 +361,18 @@
|
||||
'vehicle_state_ft': 0,
|
||||
'vehicle_state_is_user_present': 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_max': 10.333333,
|
||||
'vehicle_state_media_info_media_playback_status': 'Stopped',
|
||||
'vehicle_state_media_info_now_playing_album': '',
|
||||
'vehicle_state_media_info_now_playing_artist': '',
|
||||
'vehicle_state_media_info_now_playing_duration': 0,
|
||||
'vehicle_state_media_info_now_playing_elapsed': 0,
|
||||
'vehicle_state_media_info_now_playing_source': 'Spotify',
|
||||
'vehicle_state_media_info_now_playing_station': '',
|
||||
'vehicle_state_media_info_now_playing_title': '',
|
||||
'vehicle_state_media_info_media_playback_status': 'Playing',
|
||||
'vehicle_state_media_info_now_playing_album': 'Elon Musk',
|
||||
'vehicle_state_media_info_now_playing_artist': 'Walter Isaacson',
|
||||
'vehicle_state_media_info_now_playing_duration': 651000,
|
||||
'vehicle_state_media_info_now_playing_elapsed': 1000,
|
||||
'vehicle_state_media_info_now_playing_source': 'Audible',
|
||||
'vehicle_state_media_info_now_playing_station': 'Elon Musk',
|
||||
'vehicle_state_media_info_now_playing_title': 'Chapter 51: Cybertruck: Tesla, 2018–2019',
|
||||
'vehicle_state_media_state_remote_control_enabled': True,
|
||||
'vehicle_state_notifications_supported': True,
|
||||
'vehicle_state_odometer': 6481.019282,
|
||||
@@ -389,11 +390,11 @@
|
||||
'vehicle_state_sentry_mode_available': True,
|
||||
'vehicle_state_service_mode': 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_install_perc': 1,
|
||||
'vehicle_state_software_update_status': '',
|
||||
'vehicle_state_software_update_version': ' ',
|
||||
'vehicle_state_software_update_status': 'available',
|
||||
'vehicle_state_software_update_version': '2024.12.0.0',
|
||||
'vehicle_state_speed_limit_mode_active': False,
|
||||
'vehicle_state_speed_limit_mode_current_limit_mph': 69,
|
||||
'vehicle_state_speed_limit_mode_max_limit_mph': 120,
|
||||
|
136
tests/components/teslemetry/snapshots/test_media_player.ambr
Normal file
136
tests/components/teslemetry/snapshots/test_media_player.ambr
Normal 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, 2018–2019',
|
||||
'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, 2018–2019',
|
||||
'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',
|
||||
})
|
||||
# ---
|
461
tests/components/teslemetry/snapshots/test_number.ambr
Normal file
461
tests/components/teslemetry/snapshots/test_number.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
113
tests/components/teslemetry/snapshots/test_update.ambr
Normal file
113
tests/components/teslemetry/snapshots/test_update.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
53
tests/components/teslemetry/test_button.py
Normal file
53
tests/components/teslemetry/test_button.py
Normal 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()
|
152
tests/components/teslemetry/test_media_player.py
Normal file
152
tests/components/teslemetry/test_media_player.py
Normal 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()
|
113
tests/components/teslemetry/test_number.py
Normal file
113
tests/components/teslemetry/test_number.py
Normal 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()
|
89
tests/components/teslemetry/test_update.py
Normal file
89
tests/components/teslemetry/test_update.py
Normal 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
|
@@ -6247,3 +6247,72 @@ async def test_stopping_run_before_starting(
|
||||
# would hang indefinitely.
|
||||
run = script._ScriptRun(hass, script_obj, {}, None, True)
|
||||
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
|
||||
|
Reference in New Issue
Block a user