mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Merge branch 'dev' into esphome_bronze
This commit is contained in:
commit
96a29b9cc6
@ -52,7 +52,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
|
||||
RECOMMENDED_OPTIONS = {
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
|
||||
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
|
||||
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
|
||||
}
|
||||
|
||||
@ -134,9 +134,8 @@ class AnthropicOptionsFlow(OptionsFlow):
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if user_input[CONF_LLM_HASS_API] == "none":
|
||||
user_input.pop(CONF_LLM_HASS_API)
|
||||
|
||||
if not user_input.get(CONF_LLM_HASS_API):
|
||||
user_input.pop(CONF_LLM_HASS_API, None)
|
||||
if user_input.get(
|
||||
CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET
|
||||
) >= user_input.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS):
|
||||
@ -151,12 +150,16 @@ class AnthropicOptionsFlow(OptionsFlow):
|
||||
options = {
|
||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
||||
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
|
||||
CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API),
|
||||
}
|
||||
|
||||
suggested_values = options.copy()
|
||||
if not suggested_values.get(CONF_PROMPT):
|
||||
suggested_values[CONF_PROMPT] = llm.DEFAULT_INSTRUCTIONS_PROMPT
|
||||
if (
|
||||
suggested_llm_apis := suggested_values.get(CONF_LLM_HASS_API)
|
||||
) and isinstance(suggested_llm_apis, str):
|
||||
suggested_values[CONF_LLM_HASS_API] = [suggested_llm_apis]
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
vol.Schema(anthropic_config_option_schema(self.hass, options)),
|
||||
@ -176,24 +179,18 @@ def anthropic_config_option_schema(
|
||||
) -> dict:
|
||||
"""Return a schema for Anthropic completion options."""
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
SelectOptionDict(
|
||||
label="No control",
|
||||
value="none",
|
||||
)
|
||||
]
|
||||
hass_apis.extend(
|
||||
SelectOptionDict(
|
||||
label=api.name,
|
||||
value=api.id,
|
||||
)
|
||||
for api in llm.async_get_apis(hass)
|
||||
)
|
||||
]
|
||||
|
||||
schema = {
|
||||
vol.Optional(CONF_PROMPT): TemplateSelector(),
|
||||
vol.Optional(CONF_LLM_HASS_API, default="none"): SelectSelector(
|
||||
SelectSelectorConfig(options=hass_apis)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
|
||||
vol.Required(
|
||||
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
|
||||
): bool,
|
||||
|
@ -6,7 +6,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==2.0.0"],
|
||||
"requirements": ["pyblu==2.0.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_musc._tcp.local."
|
||||
|
@ -330,7 +330,12 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
|
||||
|
||||
if self._status.input_id is not None:
|
||||
for input_ in self._inputs:
|
||||
if input_.id == self._status.input_id:
|
||||
# the input might not have an id => also try to match on the stream_url/url
|
||||
# we have to use both because neither matches all the time
|
||||
if (
|
||||
input_.id == self._status.input_id
|
||||
or input_.url == self._status.stream_url
|
||||
):
|
||||
return input_.text
|
||||
|
||||
for preset in self._presets:
|
||||
|
@ -197,6 +197,7 @@ class ChatLog:
|
||||
(
|
||||
"?",
|
||||
";", # Greek question mark
|
||||
"?", # Chinese question mark
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -73,14 +73,14 @@ async def _async_set_position(
|
||||
Returns True if the position was set, False if there is no
|
||||
supported method for setting the position.
|
||||
"""
|
||||
if target_position == FULL_CLOSE and CoverEntityFeature.CLOSE in features:
|
||||
await service_call(SERVICE_CLOSE_COVER, service_data)
|
||||
elif target_position == FULL_OPEN and CoverEntityFeature.OPEN in features:
|
||||
await service_call(SERVICE_OPEN_COVER, service_data)
|
||||
elif CoverEntityFeature.SET_POSITION in features:
|
||||
if CoverEntityFeature.SET_POSITION in features:
|
||||
await service_call(
|
||||
SERVICE_SET_COVER_POSITION, service_data | {ATTR_POSITION: target_position}
|
||||
)
|
||||
elif target_position == FULL_CLOSE and CoverEntityFeature.CLOSE in features:
|
||||
await service_call(SERVICE_CLOSE_COVER, service_data)
|
||||
elif target_position == FULL_OPEN and CoverEntityFeature.OPEN in features:
|
||||
await service_call(SERVICE_OPEN_COVER, service_data)
|
||||
else:
|
||||
# Requested a position but the cover doesn't support it
|
||||
return False
|
||||
@ -98,15 +98,17 @@ async def _async_set_tilt_position(
|
||||
Returns True if the tilt position was set, False if there is no
|
||||
supported method for setting the tilt position.
|
||||
"""
|
||||
if target_tilt_position == FULL_CLOSE and CoverEntityFeature.CLOSE_TILT in features:
|
||||
await service_call(SERVICE_CLOSE_COVER_TILT, service_data)
|
||||
elif target_tilt_position == FULL_OPEN and CoverEntityFeature.OPEN_TILT in features:
|
||||
await service_call(SERVICE_OPEN_COVER_TILT, service_data)
|
||||
elif CoverEntityFeature.SET_TILT_POSITION in features:
|
||||
if CoverEntityFeature.SET_TILT_POSITION in features:
|
||||
await service_call(
|
||||
SERVICE_SET_COVER_TILT_POSITION,
|
||||
service_data | {ATTR_TILT_POSITION: target_tilt_position},
|
||||
)
|
||||
elif (
|
||||
target_tilt_position == FULL_CLOSE and CoverEntityFeature.CLOSE_TILT in features
|
||||
):
|
||||
await service_call(SERVICE_CLOSE_COVER_TILT, service_data)
|
||||
elif target_tilt_position == FULL_OPEN and CoverEntityFeature.OPEN_TILT in features:
|
||||
await service_call(SERVICE_OPEN_COVER_TILT, service_data)
|
||||
else:
|
||||
# Requested a tilt position but the cover doesn't support it
|
||||
return False
|
||||
@ -183,12 +185,12 @@ async def _async_reproduce_state(
|
||||
current_attrs = cur_state.attributes
|
||||
target_attrs = state.attributes
|
||||
|
||||
current_position = current_attrs.get(ATTR_CURRENT_POSITION)
|
||||
target_position = target_attrs.get(ATTR_CURRENT_POSITION)
|
||||
current_position: int | None = current_attrs.get(ATTR_CURRENT_POSITION)
|
||||
target_position: int | None = target_attrs.get(ATTR_CURRENT_POSITION)
|
||||
position_matches = current_position == target_position
|
||||
|
||||
current_tilt_position = current_attrs.get(ATTR_CURRENT_TILT_POSITION)
|
||||
target_tilt_position = target_attrs.get(ATTR_CURRENT_TILT_POSITION)
|
||||
current_tilt_position: int | None = current_attrs.get(ATTR_CURRENT_TILT_POSITION)
|
||||
target_tilt_position: int | None = target_attrs.get(ATTR_CURRENT_TILT_POSITION)
|
||||
tilt_position_matches = current_tilt_position == target_tilt_position
|
||||
|
||||
state_matches = cur_state.state == target_state
|
||||
@ -214,19 +216,11 @@ async def _async_reproduce_state(
|
||||
)
|
||||
service_data = {ATTR_ENTITY_ID: entity_id}
|
||||
|
||||
set_position = (
|
||||
not position_matches
|
||||
and target_position is not None
|
||||
and await _async_set_position(
|
||||
service_call, service_data, features, target_position
|
||||
)
|
||||
set_position = target_position is not None and await _async_set_position(
|
||||
service_call, service_data, features, target_position
|
||||
)
|
||||
set_tilt = (
|
||||
not tilt_position_matches
|
||||
and target_tilt_position is not None
|
||||
and await _async_set_tilt_position(
|
||||
service_call, service_data, features, target_tilt_position
|
||||
)
|
||||
set_tilt = target_tilt_position is not None and await _async_set_tilt_position(
|
||||
service_call, service_data, features, target_tilt_position
|
||||
)
|
||||
|
||||
if target_state in CLOSING_STATES:
|
||||
|
@ -23,6 +23,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
@ -44,6 +45,7 @@ from .const import (
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
DEFAULT_ALLOW_SERVICE_CALLS,
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
)
|
||||
from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info
|
||||
@ -63,6 +65,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
|
||||
_reauth_entry: ConfigEntry
|
||||
_reconfig_entry: ConfigEntry
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize flow."""
|
||||
@ -88,7 +91,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
fields: dict[Any, type] = OrderedDict()
|
||||
fields[vol.Required(CONF_HOST, default=self._host or vol.UNDEFINED)] = str
|
||||
fields[vol.Optional(CONF_PORT, default=self._port or 6053)] = int
|
||||
fields[vol.Optional(CONF_PORT, default=self._port or DEFAULT_PORT)] = int
|
||||
|
||||
errors = {}
|
||||
if error is not None:
|
||||
@ -140,7 +143,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle reauthorization flow when encryption was removed."""
|
||||
if user_input is not None:
|
||||
self._noise_psk = None
|
||||
return await self._async_get_entry_or_resolve_conflict()
|
||||
return await self._async_validated_connection()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_encryption_removed_confirm",
|
||||
@ -172,6 +175,18 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders={"name": self._name},
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by a reconfig request."""
|
||||
self._reconfig_entry = self._get_reconfigure_entry()
|
||||
data = self._reconfig_entry.data
|
||||
self._host = data[CONF_HOST]
|
||||
self._port = data.get(CONF_PORT, DEFAULT_PORT)
|
||||
self._noise_psk = data.get(CONF_NOISE_PSK)
|
||||
self._device_name = data.get(CONF_DEVICE_NAME)
|
||||
return await self._async_step_user_base()
|
||||
|
||||
@property
|
||||
def _name(self) -> str:
|
||||
return self.__name or "ESPHome"
|
||||
@ -230,7 +245,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_authenticate()
|
||||
|
||||
self._password = ""
|
||||
return await self._async_get_entry_or_resolve_conflict()
|
||||
return await self._async_validated_connection()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@ -270,13 +285,13 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
await self._async_validate_mac_abort_configured(
|
||||
mac_address, self._host, self._port
|
||||
)
|
||||
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def _async_validate_mac_abort_configured(
|
||||
self, formatted_mac: str, host: str, port: int | None
|
||||
) -> None:
|
||||
"""Validate if the MAC address is already configured."""
|
||||
assert self.unique_id is not None
|
||||
if not (
|
||||
entry := self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||
self.handler, formatted_mac
|
||||
@ -393,7 +408,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
data={
|
||||
**self._entry_with_name_conflict.data,
|
||||
CONF_HOST: self._host,
|
||||
CONF_PORT: self._port or 6053,
|
||||
CONF_PORT: self._port or DEFAULT_PORT,
|
||||
CONF_PASSWORD: self._password or "",
|
||||
CONF_NOISE_PSK: self._noise_psk or "",
|
||||
},
|
||||
@ -417,20 +432,24 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
await self.hass.config_entries.async_remove(
|
||||
self._entry_with_name_conflict.entry_id
|
||||
)
|
||||
return self._async_get_entry()
|
||||
|
||||
async def _async_get_entry_or_resolve_conflict(self) -> ConfigFlowResult:
|
||||
"""Return the entry or resolve a conflict."""
|
||||
if self.source != SOURCE_REAUTH:
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if entry.data.get(CONF_DEVICE_NAME) == self._device_name:
|
||||
self._entry_with_name_conflict = entry
|
||||
return await self.async_step_name_conflict()
|
||||
return self._async_get_entry()
|
||||
return self._async_create_entry()
|
||||
|
||||
@callback
|
||||
def _async_get_entry(self) -> ConfigFlowResult:
|
||||
config_data = {
|
||||
def _async_create_entry(self) -> ConfigFlowResult:
|
||||
"""Create the config entry."""
|
||||
assert self._name is not None
|
||||
return self.async_create_entry(
|
||||
title=self._name,
|
||||
data=self._async_make_config_data(),
|
||||
options={
|
||||
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
},
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_make_config_data(self) -> dict[str, Any]:
|
||||
"""Return config data for the entry."""
|
||||
return {
|
||||
CONF_HOST: self._host,
|
||||
CONF_PORT: self._port,
|
||||
# The API uses protobuf, so empty string denotes absence
|
||||
@ -438,19 +457,99 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_NOISE_PSK: self._noise_psk or "",
|
||||
CONF_DEVICE_NAME: self._device_name,
|
||||
}
|
||||
config_options = {
|
||||
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
}
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._reauth_entry, data=self._reauth_entry.data | config_data
|
||||
)
|
||||
|
||||
assert self._name is not None
|
||||
return self.async_create_entry(
|
||||
title=self._name,
|
||||
data=config_data,
|
||||
options=config_options,
|
||||
async def _async_validated_connection(self) -> ConfigFlowResult:
|
||||
"""Handle validated connection."""
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
return await self._async_reconfig_validated_connection()
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return await self._async_reauth_validated_connection()
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if entry.data.get(CONF_DEVICE_NAME) == self._device_name:
|
||||
self._entry_with_name_conflict = entry
|
||||
return await self.async_step_name_conflict()
|
||||
return self._async_create_entry()
|
||||
|
||||
async def _async_reauth_validated_connection(self) -> ConfigFlowResult:
|
||||
"""Handle reauth validated connection."""
|
||||
assert self._reauth_entry.unique_id is not None
|
||||
if self.unique_id == self._reauth_entry.unique_id:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._reauth_entry,
|
||||
data=self._reauth_entry.data | self._async_make_config_data(),
|
||||
)
|
||||
assert self._host is not None
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: self._host,
|
||||
CONF_PORT: self._port,
|
||||
CONF_NOISE_PSK: self._noise_psk,
|
||||
}
|
||||
)
|
||||
# Reauth was triggered a while ago, and since than
|
||||
# a new device resides at the same IP address.
|
||||
assert self._device_name is not None
|
||||
return self.async_abort(
|
||||
reason="reauth_unique_id_changed",
|
||||
description_placeholders={
|
||||
"name": self._reauth_entry.data.get(
|
||||
CONF_DEVICE_NAME, self._reauth_entry.title
|
||||
),
|
||||
"host": self._host,
|
||||
"expected_mac": format_mac(self._reauth_entry.unique_id),
|
||||
"unexpected_mac": format_mac(self.unique_id),
|
||||
"unexpected_device_name": self._device_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def _async_reconfig_validated_connection(self) -> ConfigFlowResult:
|
||||
"""Handle reconfigure validated connection."""
|
||||
assert self._reconfig_entry.unique_id is not None
|
||||
assert self._host is not None
|
||||
assert self._device_name is not None
|
||||
if not (
|
||||
unique_id_matches := (self.unique_id == self._reconfig_entry.unique_id)
|
||||
):
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: self._host,
|
||||
CONF_PORT: self._port,
|
||||
CONF_NOISE_PSK: self._noise_psk,
|
||||
}
|
||||
)
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if (
|
||||
entry.entry_id != self._reconfig_entry.entry_id
|
||||
and entry.data.get(CONF_DEVICE_NAME) == self._device_name
|
||||
):
|
||||
return self.async_abort(
|
||||
reason="reconfigure_name_conflict",
|
||||
description_placeholders={
|
||||
"name": self._reconfig_entry.data[CONF_DEVICE_NAME],
|
||||
"host": self._host,
|
||||
"expected_mac": format_mac(self._reconfig_entry.unique_id),
|
||||
"existing_title": entry.title,
|
||||
},
|
||||
)
|
||||
if unique_id_matches:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._reconfig_entry,
|
||||
data=self._reconfig_entry.data | self._async_make_config_data(),
|
||||
)
|
||||
if self._reconfig_entry.data.get(CONF_DEVICE_NAME) == self._device_name:
|
||||
self._entry_with_name_conflict = self._reconfig_entry
|
||||
return await self.async_step_name_conflict()
|
||||
return self.async_abort(
|
||||
reason="reconfigure_unique_id_changed",
|
||||
description_placeholders={
|
||||
"name": self._reconfig_entry.data.get(
|
||||
CONF_DEVICE_NAME, self._reconfig_entry.title
|
||||
),
|
||||
"host": self._host,
|
||||
"expected_mac": format_mac(self._reconfig_entry.unique_id),
|
||||
"unexpected_mac": format_mac(self.unique_id),
|
||||
"unexpected_device_name": self._device_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_encryption_key(
|
||||
@ -481,7 +580,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
error = await self.try_login()
|
||||
if error:
|
||||
return await self.async_step_authenticate(error=error)
|
||||
return await self._async_get_entry_or_resolve_conflict()
|
||||
return await self._async_validated_connection()
|
||||
|
||||
errors = {}
|
||||
if error is not None:
|
||||
@ -501,12 +600,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
zeroconf_instance = await zeroconf.async_get_instance(self.hass)
|
||||
cli = APIClient(
|
||||
host,
|
||||
port or 6053,
|
||||
port or DEFAULT_PORT,
|
||||
"",
|
||||
zeroconf_instance=zeroconf_instance,
|
||||
noise_psk=noise_psk,
|
||||
)
|
||||
|
||||
try:
|
||||
await cli.connect()
|
||||
self._device_info = await cli.device_info()
|
||||
@ -541,9 +639,13 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
assert self._device_info is not None
|
||||
mac_address = format_mac(self._device_info.mac_address)
|
||||
await self.async_set_unique_id(mac_address, raise_on_progress=False)
|
||||
if self.source != SOURCE_REAUTH:
|
||||
if self.source not in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: self._host, CONF_PORT: self._port}
|
||||
updates={
|
||||
CONF_HOST: self._host,
|
||||
CONF_PORT: self._port,
|
||||
CONF_NOISE_PSK: self._noise_psk,
|
||||
}
|
||||
)
|
||||
|
||||
return None
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""ESPHome constants."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
|
||||
DOMAIN = "esphome"
|
||||
@ -13,6 +15,7 @@ CONF_BLUETOOTH_MAC_ADDRESS = "bluetooth_mac_address"
|
||||
DEFAULT_ALLOW_SERVICE_CALLS = True
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||
|
||||
DEFAULT_PORT: Final = 6053
|
||||
|
||||
STABLE_BLE_VERSION_STR = "2025.2.2"
|
||||
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
|
||||
|
@ -198,6 +198,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
"""Define a base esphome entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
@ -223,24 +224,6 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
#
|
||||
# If `friendly_name` is set, we use the Friendly naming rules, if
|
||||
# `friendly_name` is not set we make an exception to the naming rules for
|
||||
# backwards compatibility and use the Legacy naming rules.
|
||||
#
|
||||
# Friendly naming
|
||||
# - Friendly name is prepended to entity names
|
||||
# - Device Name is prepended to entity ids
|
||||
# - Entity id is constructed from device name and object id
|
||||
#
|
||||
# Legacy naming
|
||||
# - Device name is not prepended to entity names
|
||||
# - Device name is not prepended to entity ids
|
||||
# - Entity id is constructed from entity name
|
||||
#
|
||||
if not device_info.friendly_name:
|
||||
return
|
||||
self._attr_has_entity_name = True
|
||||
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
@ -520,6 +520,15 @@ class ESPHomeManager:
|
||||
if device_info.name:
|
||||
reconnect_logic.name = device_info.name
|
||||
|
||||
if not device_info.friendly_name:
|
||||
_LOGGER.info(
|
||||
"No `friendly_name` set in the `esphome:` section of the "
|
||||
"YAML config for device '%s' (MAC: %s); It's recommended "
|
||||
"to add one for easier identification and better alignment "
|
||||
"with Home Assistant naming conventions",
|
||||
device_info.name,
|
||||
device_mac,
|
||||
)
|
||||
self.device_id = _async_setup_device_registry(hass, entry, entry_data)
|
||||
|
||||
entry_data.async_update_device_state()
|
||||
@ -756,7 +765,7 @@ def _async_setup_device_registry(
|
||||
config_entry_id=entry.entry_id,
|
||||
configuration_url=configuration_url,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)},
|
||||
name=entry_data.friendly_name,
|
||||
name=entry_data.friendly_name or entry_data.name,
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
sw_version=sw_version,
|
||||
|
@ -10,7 +10,11 @@
|
||||
"mqtt_missing_api": "Missing API port in MQTT properties.",
|
||||
"mqtt_missing_ip": "Missing IP address in MQTT properties.",
|
||||
"mqtt_missing_payload": "Missing MQTT Payload.",
|
||||
"name_conflict_migrated": "The configuration for `{name}` has been migrated to a new device with MAC address `{mac}` from `{existing_mac}`."
|
||||
"name_conflict_migrated": "The configuration for `{name}` has been migrated to a new device with MAC address `{mac}` from `{existing_mac}`.",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"reauth_unique_id_changed": "**Re-authentication of `{name}` was aborted** because the address `{host}` points to a different device: `{unexpected_device_name}` (MAC: `{unexpected_mac}`) instead of the expected one (MAC: `{expected_mac}`).",
|
||||
"reconfigure_name_conflict": "**Reconfiguration of `{name}` was aborted** because the address `{host}` points to a device named `{name}` (MAC: `{expected_mac}`), which is already in use by another configuration entry: `{existing_title}`.",
|
||||
"reconfigure_unique_id_changed": "**Reconfiguration of `{name}` was aborted** because the address `{host}` points to a different device: `{unexpected_device_name}` (MAC: `{unexpected_mac}`) instead of the expected one (MAC: `{expected_mac}`)."
|
||||
},
|
||||
"error": {
|
||||
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",
|
||||
|
@ -183,10 +183,10 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if user_input[CONF_LLM_HASS_API] == "none":
|
||||
user_input.pop(CONF_LLM_HASS_API)
|
||||
if not user_input.get(CONF_LLM_HASS_API):
|
||||
user_input.pop(CONF_LLM_HASS_API, None)
|
||||
if not (
|
||||
user_input.get(CONF_LLM_HASS_API, "none") != "none"
|
||||
user_input.get(CONF_LLM_HASS_API)
|
||||
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
|
||||
):
|
||||
# Don't allow to save options that enable the Google Seearch tool with an Assist API
|
||||
@ -213,18 +213,16 @@ async def google_generative_ai_config_option_schema(
|
||||
) -> dict:
|
||||
"""Return a schema for Google Generative AI completion options."""
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
SelectOptionDict(
|
||||
label="No control",
|
||||
value="none",
|
||||
)
|
||||
]
|
||||
hass_apis.extend(
|
||||
SelectOptionDict(
|
||||
label=api.name,
|
||||
value=api.id,
|
||||
)
|
||||
for api in llm.async_get_apis(hass)
|
||||
)
|
||||
]
|
||||
if (suggested_llm_apis := options.get(CONF_LLM_HASS_API)) and isinstance(
|
||||
suggested_llm_apis, str
|
||||
):
|
||||
suggested_llm_apis = [suggested_llm_apis]
|
||||
|
||||
schema = {
|
||||
vol.Optional(
|
||||
@ -237,9 +235,8 @@ async def google_generative_ai_config_option_schema(
|
||||
): TemplateSelector(),
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
description={"suggested_value": options.get(CONF_LLM_HASS_API)},
|
||||
default="none",
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis)),
|
||||
description={"suggested_value": suggested_llm_apis},
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
|
||||
vol.Required(
|
||||
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
|
||||
): bool,
|
||||
|
@ -46,6 +46,8 @@ RESPONSE_HEADERS_FILTER = {
|
||||
MIN_COMPRESSED_SIZE = 128
|
||||
MAX_SIMPLE_RESPONSE_SIZE = 4194000
|
||||
|
||||
DISABLED_TIMEOUT = ClientTimeout(total=None)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_ingress_view(hass: HomeAssistant, host: str) -> None:
|
||||
@ -167,7 +169,7 @@ class HassIOIngress(HomeAssistantView):
|
||||
params=request.query,
|
||||
allow_redirects=False,
|
||||
data=request.content if request.method != "GET" else None,
|
||||
timeout=ClientTimeout(total=None),
|
||||
timeout=DISABLED_TIMEOUT,
|
||||
skip_auto_headers={hdrs.CONTENT_TYPE},
|
||||
) as result:
|
||||
headers = _response_header(result)
|
||||
|
@ -52,7 +52,7 @@ class HistoryLiveStream:
|
||||
subscriptions: list[CALLBACK_TYPE]
|
||||
end_time_unsub: CALLBACK_TYPE | None = None
|
||||
task: asyncio.Task | None = None
|
||||
wait_sync_task: asyncio.Task | None = None
|
||||
wait_sync_future: asyncio.Future[None] | None = None
|
||||
|
||||
|
||||
@callback
|
||||
@ -491,8 +491,8 @@ async def ws_stream(
|
||||
subscriptions.clear()
|
||||
if live_stream.task:
|
||||
live_stream.task.cancel()
|
||||
if live_stream.wait_sync_task:
|
||||
live_stream.wait_sync_task.cancel()
|
||||
if live_stream.wait_sync_future:
|
||||
live_stream.wait_sync_future.cancel()
|
||||
if live_stream.end_time_unsub:
|
||||
live_stream.end_time_unsub()
|
||||
live_stream.end_time_unsub = None
|
||||
@ -554,10 +554,12 @@ async def ws_stream(
|
||||
)
|
||||
)
|
||||
|
||||
live_stream.wait_sync_task = create_eager_task(
|
||||
get_instance(hass).async_block_till_done()
|
||||
)
|
||||
await live_stream.wait_sync_task
|
||||
if sync_future := get_instance(hass).async_get_commit_future():
|
||||
# Set the future so we can cancel it if the client
|
||||
# unsubscribes before the commit is done so we don't
|
||||
# query the database needlessly
|
||||
live_stream.wait_sync_future = sync_future
|
||||
await live_stream.wait_sync_future
|
||||
|
||||
#
|
||||
# Fetch any states from the database that have
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, EntityCategory, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@ -43,7 +43,6 @@ async def async_setup_entry(
|
||||
class InComfortClimate(IncomfortEntity, ClimateEntity):
|
||||
"""Representation of an InComfort/InTouch climate device."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_min_temp = 5.0
|
||||
_attr_max_temp = 30.0
|
||||
_attr_name = None
|
||||
|
@ -197,6 +197,11 @@ class JewishCalendarSensor(JewishCalendarEntity, SensorEntity):
|
||||
super().__init__(config_entry, description)
|
||||
self._attrs: dict[str, str] = {}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self.async_update()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the state of the sensor."""
|
||||
now = dt_util.now()
|
||||
|
@ -40,6 +40,7 @@ PLATFORMS = [
|
||||
Platform.CALENDAR,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.UPDATE,
|
||||
]
|
||||
|
@ -4,8 +4,9 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from pylamarzocco import LaMarzoccoMachine
|
||||
from pylamarzocco.const import BackFlushStatus, MachineState, WidgetType
|
||||
from pylamarzocco.models import BackFlush, BaseWidgetOutput, MachineStatus
|
||||
from pylamarzocco.models import BackFlush, MachineStatus
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@ -30,7 +31,7 @@ class LaMarzoccoBinarySensorEntityDescription(
|
||||
):
|
||||
"""Description of a La Marzocco binary sensor."""
|
||||
|
||||
is_on_fn: Callable[[dict[WidgetType, BaseWidgetOutput]], bool | None]
|
||||
is_on_fn: Callable[[LaMarzoccoMachine], bool | None]
|
||||
|
||||
|
||||
ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
|
||||
@ -38,7 +39,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
|
||||
key="water_tank",
|
||||
translation_key="water_tank",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
is_on_fn=lambda config: WidgetType.CM_NO_WATER in config,
|
||||
is_on_fn=lambda machine: WidgetType.CM_NO_WATER in machine.dashboard.config,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
LaMarzoccoBinarySensorEntityDescription(
|
||||
@ -46,8 +47,8 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
|
||||
translation_key="brew_active",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
is_on_fn=(
|
||||
lambda config: cast(
|
||||
MachineStatus, config[WidgetType.CM_MACHINE_STATUS]
|
||||
lambda machine: cast(
|
||||
MachineStatus, machine.dashboard.config[WidgetType.CM_MACHINE_STATUS]
|
||||
).status
|
||||
is MachineState.BREWING
|
||||
),
|
||||
@ -59,11 +60,21 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
|
||||
translation_key="backflush_enabled",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
is_on_fn=(
|
||||
lambda config: cast(BackFlush, config[WidgetType.CM_BACK_FLUSH]).status
|
||||
lambda machine: cast(
|
||||
BackFlush, machine.dashboard.config[WidgetType.CM_BACK_FLUSH]
|
||||
).status
|
||||
is BackFlushStatus.REQUESTED
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
LaMarzoccoBinarySensorEntityDescription(
|
||||
key="websocket_connected",
|
||||
translation_key="websocket_connected",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
is_on_fn=(lambda machine: machine.websocket.connected),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -90,6 +101,4 @@ class LaMarzoccoBinarySensorEntity(LaMarzoccoEntity, BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.entity_description.is_on_fn(
|
||||
self.coordinator.device.dashboard.config
|
||||
)
|
||||
return self.entity_description.is_on_fn(self.coordinator.device)
|
||||
|
@ -49,6 +49,7 @@ from .const import CONF_USE_BLUETOOTH, DOMAIN
|
||||
from .coordinator import LaMarzoccoConfigEntry
|
||||
|
||||
CONF_MACHINE = "machine"
|
||||
BT_MODEL_PREFIXES = ("MICRA", "MINI", "GS3")
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -105,7 +106,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._config = data
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data=data
|
||||
self._get_reauth_entry(), data_updates=data
|
||||
)
|
||||
if self._discovered:
|
||||
if self._discovered[CONF_MACHINE] not in self._things:
|
||||
@ -169,10 +170,15 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not errors:
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
for service_info in async_discovered_service_info(self.hass):
|
||||
self._discovered[service_info.name] = service_info.address
|
||||
if service_info.name.startswith(BT_MODEL_PREFIXES):
|
||||
self._discovered[service_info.name] = service_info.address
|
||||
|
||||
if self._discovered:
|
||||
return await self.async_step_bluetooth_selection()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates=self._config,
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=selected_device.name,
|
||||
@ -217,8 +223,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data={
|
||||
**self._config,
|
||||
data_updates={
|
||||
CONF_MAC: user_input[CONF_MAC],
|
||||
},
|
||||
)
|
||||
|
@ -36,6 +36,15 @@
|
||||
},
|
||||
"smart_standby_time": {
|
||||
"default": "mdi:timer"
|
||||
},
|
||||
"preinfusion_time": {
|
||||
"default": "mdi:water"
|
||||
},
|
||||
"prebrew_time_on": {
|
||||
"default": "mdi:water"
|
||||
},
|
||||
"prebrew_time_off": {
|
||||
"default": "mdi:water-off"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@ -63,6 +72,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"coffee_boiler_ready_time": {
|
||||
"default": "mdi:av-timer"
|
||||
},
|
||||
"steam_boiler_ready_time": {
|
||||
"default": "mdi:av-timer"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"main": {
|
||||
"default": "mdi:power",
|
||||
|
@ -5,9 +5,9 @@ from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from pylamarzocco import LaMarzoccoMachine
|
||||
from pylamarzocco.const import WidgetType
|
||||
from pylamarzocco.const import ModelName, PreExtractionMode, WidgetType
|
||||
from pylamarzocco.exceptions import RequestNotSuccessful
|
||||
from pylamarzocco.models import CoffeeBoiler
|
||||
from pylamarzocco.models import CoffeeBoiler, PreBrewing
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
@ -77,6 +77,123 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
),
|
||||
native_value_fn=lambda machine: machine.schedule.smart_wake_up_sleep.smart_stand_by_minutes,
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
key="preinfusion_off",
|
||||
translation_key="preinfusion_time",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_pre_extraction_times(
|
||||
seconds_on=0,
|
||||
seconds_off=float(value),
|
||||
)
|
||||
),
|
||||
native_value_fn=(
|
||||
lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
)
|
||||
.times.pre_infusion[0]
|
||||
.seconds.seconds_out
|
||||
),
|
||||
available_fn=(
|
||||
lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
).mode
|
||||
is PreExtractionMode.PREINFUSION
|
||||
),
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (
|
||||
ModelName.LINEA_MICRA,
|
||||
ModelName.LINEA_MINI,
|
||||
ModelName.LINEA_MINI_R,
|
||||
)
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
key="prebrew_on",
|
||||
translation_key="prebrew_time_on",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_pre_extraction_times(
|
||||
seconds_on=float(value),
|
||||
seconds_off=cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
)
|
||||
.times.pre_brewing[0]
|
||||
.seconds.seconds_out,
|
||||
)
|
||||
),
|
||||
native_value_fn=(
|
||||
lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
)
|
||||
.times.pre_brewing[0]
|
||||
.seconds.seconds_in
|
||||
),
|
||||
available_fn=lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
).mode
|
||||
is PreExtractionMode.PREBREWING,
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (
|
||||
ModelName.LINEA_MICRA,
|
||||
ModelName.LINEA_MINI,
|
||||
ModelName.LINEA_MINI_R,
|
||||
)
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
key="prebrew_off",
|
||||
translation_key="prebrew_time_off",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_pre_extraction_times(
|
||||
seconds_on=cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
)
|
||||
.times.pre_brewing[0]
|
||||
.seconds.seconds_in,
|
||||
seconds_off=float(value),
|
||||
)
|
||||
),
|
||||
native_value_fn=(
|
||||
lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
)
|
||||
.times.pre_brewing[0]
|
||||
.seconds.seconds_out
|
||||
),
|
||||
available_fn=(
|
||||
lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
).mode
|
||||
is PreExtractionMode.PREBREWING
|
||||
),
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (
|
||||
ModelName.LINEA_MICRA,
|
||||
ModelName.LINEA_MINI,
|
||||
ModelName.LINEA_MINI_R,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
115
homeassistant/components/lamarzocco/sensor.py
Normal file
115
homeassistant/components/lamarzocco/sensor.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""Sensor platform for La Marzocco espresso machines."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import cast
|
||||
|
||||
from pylamarzocco.const import ModelName, WidgetType
|
||||
from pylamarzocco.models import (
|
||||
BaseWidgetOutput,
|
||||
CoffeeBoiler,
|
||||
SteamBoilerLevel,
|
||||
SteamBoilerTemperature,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import LaMarzoccoConfigEntry
|
||||
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LaMarzoccoSensorEntityDescription(
|
||||
LaMarzoccoEntityDescription,
|
||||
SensorEntityDescription,
|
||||
):
|
||||
"""Description of a La Marzocco sensor."""
|
||||
|
||||
value_fn: Callable[
|
||||
[dict[WidgetType, BaseWidgetOutput]], StateType | datetime | None
|
||||
]
|
||||
|
||||
|
||||
ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
|
||||
LaMarzoccoSensorEntityDescription(
|
||||
key="coffee_boiler_ready_time",
|
||||
translation_key="coffee_boiler_ready_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=(
|
||||
lambda config: cast(
|
||||
CoffeeBoiler, config[WidgetType.CM_COFFEE_BOILER]
|
||||
).ready_start_time
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
LaMarzoccoSensorEntityDescription(
|
||||
key="steam_boiler_ready_time",
|
||||
translation_key="steam_boiler_ready_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=(
|
||||
lambda config: cast(
|
||||
SteamBoilerLevel, config[WidgetType.CM_STEAM_BOILER_LEVEL]
|
||||
).ready_start_time
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MICRA, ModelName.LINEA_MINI_R)
|
||||
),
|
||||
),
|
||||
LaMarzoccoSensorEntityDescription(
|
||||
key="steam_boiler_ready_time",
|
||||
translation_key="steam_boiler_ready_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=(
|
||||
lambda config: cast(
|
||||
SteamBoilerTemperature, config[WidgetType.CM_STEAM_BOILER_TEMPERATURE]
|
||||
).ready_start_time
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.GS3_AV, ModelName.GS3_MP, ModelName.LINEA_MINI)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LaMarzoccoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensor entities."""
|
||||
coordinator = entry.runtime_data.config_coordinator
|
||||
|
||||
async_add_entities(
|
||||
LaMarzoccoSensorEntity(coordinator, description)
|
||||
for description in ENTITIES
|
||||
if description.supported_fn(coordinator)
|
||||
)
|
||||
|
||||
|
||||
class LaMarzoccoSensorEntity(LaMarzoccoEntity, SensorEntity):
|
||||
"""Sensor representing espresso machine water reservoir status."""
|
||||
|
||||
entity_description: LaMarzoccoSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime | None:
|
||||
"""Return value of the sensor."""
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.device.dashboard.config
|
||||
)
|
@ -83,6 +83,9 @@
|
||||
},
|
||||
"water_tank": {
|
||||
"name": "Water tank empty"
|
||||
},
|
||||
"websocket_connected": {
|
||||
"name": "WebSocket connected"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@ -101,6 +104,15 @@
|
||||
},
|
||||
"smart_standby_time": {
|
||||
"name": "Smart standby time"
|
||||
},
|
||||
"preinfusion_time": {
|
||||
"name": "Preinfusion time"
|
||||
},
|
||||
"prebrew_time_on": {
|
||||
"name": "Prebrew on time"
|
||||
},
|
||||
"prebrew_time_off": {
|
||||
"name": "Prebrew off time"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@ -128,6 +140,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"coffee_boiler_ready_time": {
|
||||
"name": "Coffee boiler ready time"
|
||||
},
|
||||
"steam_boiler_ready_time": {
|
||||
"name": "Steam boiler ready time"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"auto_on_off": {
|
||||
"name": "Auto on/off ({id})"
|
||||
|
@ -1,9 +1,10 @@
|
||||
"""Support for La Marzocco update entities."""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pylamarzocco.const import FirmwareType
|
||||
from pylamarzocco.const import FirmwareType, UpdateCommandStatus
|
||||
from pylamarzocco.exceptions import RequestNotSuccessful
|
||||
|
||||
from homeassistant.components.update import (
|
||||
@ -22,6 +23,7 @@ from .coordinator import LaMarzoccoConfigEntry
|
||||
from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
MAX_UPDATE_WAIT = 150
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@ -71,7 +73,11 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
|
||||
"""Entity representing the update state."""
|
||||
|
||||
entity_description: LaMarzoccoUpdateEntityDescription
|
||||
_attr_supported_features = UpdateEntityFeature.INSTALL
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL
|
||||
| UpdateEntityFeature.PROGRESS
|
||||
| UpdateEntityFeature.RELEASE_NOTES
|
||||
)
|
||||
|
||||
@property
|
||||
def installed_version(self) -> str:
|
||||
@ -94,15 +100,40 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
|
||||
"""Return the release notes URL."""
|
||||
return "https://support-iot.lamarzocco.com/firmware-updates/"
|
||||
|
||||
def release_notes(self) -> str | None:
|
||||
"""Return the release notes for the latest firmware version."""
|
||||
if available_update := self.coordinator.device.settings.firmwares[
|
||||
self.entity_description.component
|
||||
].available_update:
|
||||
return available_update.change_log
|
||||
return None
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
"""Install an update."""
|
||||
|
||||
self._attr_in_progress = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
counter = 0
|
||||
|
||||
def _raise_timeout_error() -> None: # to avoid TRY301
|
||||
raise TimeoutError("Update timed out")
|
||||
|
||||
try:
|
||||
await self.coordinator.device.update_firmware()
|
||||
except RequestNotSuccessful as exc:
|
||||
while (
|
||||
update_progress := await self.coordinator.device.get_firmware()
|
||||
).command_status is UpdateCommandStatus.IN_PROGRESS:
|
||||
if counter >= MAX_UPDATE_WAIT:
|
||||
_raise_timeout_error()
|
||||
self._attr_update_percentage = update_progress.progress_percentage
|
||||
self.async_write_ha_state()
|
||||
await asyncio.sleep(3)
|
||||
counter += 1
|
||||
|
||||
except (TimeoutError, RequestNotSuccessful) as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
@ -110,5 +141,6 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
|
||||
"key": self.entity_description.key,
|
||||
},
|
||||
) from exc
|
||||
self._attr_in_progress = False
|
||||
await self.coordinator.async_request_refresh()
|
||||
finally:
|
||||
self._attr_in_progress = False
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
@ -22,7 +22,7 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from .const import CONF_CONNECT_CLIENT_ID, MQTT_SUBSCRIPTION_INTERVAL
|
||||
from .const import CONF_CONNECT_CLIENT_ID, DOMAIN, MQTT_SUBSCRIPTION_INTERVAL
|
||||
from .coordinator import DeviceDataUpdateCoordinator, async_setup_device_coordinator
|
||||
from .mqtt import ThinQMQTT
|
||||
|
||||
@ -137,7 +137,15 @@ async def async_setup_mqtt(
|
||||
entry.runtime_data.mqtt_client = mqtt_client
|
||||
|
||||
# Try to connect.
|
||||
result = await mqtt_client.async_connect()
|
||||
try:
|
||||
result = await mqtt_client.async_connect()
|
||||
except (AttributeError, ThinQAPIException, TypeError, ValueError) as exc:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="failed_to_connect_mqtt",
|
||||
translation_placeholders={"error": str(exc)},
|
||||
) from exc
|
||||
|
||||
if not result:
|
||||
_LOGGER.error("Failed to set up mqtt connection")
|
||||
return
|
||||
|
@ -43,19 +43,16 @@ class ThinQMQTT:
|
||||
|
||||
async def async_connect(self) -> bool:
|
||||
"""Create a mqtt client and then try to connect."""
|
||||
try:
|
||||
self.client = await ThinQMQTTClient(
|
||||
self.thinq_api, self.client_id, self.on_message_received
|
||||
)
|
||||
if self.client is None:
|
||||
return False
|
||||
|
||||
# Connect to server and create certificate.
|
||||
return await self.client.async_prepare_mqtt()
|
||||
except (ThinQAPIException, TypeError, ValueError):
|
||||
_LOGGER.exception("Failed to connect")
|
||||
self.client = await ThinQMQTTClient(
|
||||
self.thinq_api, self.client_id, self.on_message_received
|
||||
)
|
||||
if self.client is None:
|
||||
return False
|
||||
|
||||
# Connect to server and create certificate.
|
||||
return await self.client.async_prepare_mqtt()
|
||||
|
||||
async def async_disconnect(self, event: Event | None = None) -> None:
|
||||
"""Unregister client and disconnects handlers."""
|
||||
await self.async_end_subscribes()
|
||||
|
@ -1034,5 +1034,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"failed_to_connect_mqtt": {
|
||||
"message": "Failed to connect MQTT: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ class LogbookLiveStream:
|
||||
subscriptions: list[CALLBACK_TYPE]
|
||||
end_time_unsub: CALLBACK_TYPE | None = None
|
||||
task: asyncio.Task | None = None
|
||||
wait_sync_task: asyncio.Task | None = None
|
||||
wait_sync_future: asyncio.Future[None] | None = None
|
||||
|
||||
|
||||
@callback
|
||||
@ -329,8 +329,8 @@ async def ws_event_stream(
|
||||
subscriptions.clear()
|
||||
if live_stream.task:
|
||||
live_stream.task.cancel()
|
||||
if live_stream.wait_sync_task:
|
||||
live_stream.wait_sync_task.cancel()
|
||||
if live_stream.wait_sync_future:
|
||||
live_stream.wait_sync_future.cancel()
|
||||
if live_stream.end_time_unsub:
|
||||
live_stream.end_time_unsub()
|
||||
live_stream.end_time_unsub = None
|
||||
@ -399,10 +399,12 @@ async def ws_event_stream(
|
||||
)
|
||||
)
|
||||
|
||||
live_stream.wait_sync_task = create_eager_task(
|
||||
get_instance(hass).async_block_till_done()
|
||||
)
|
||||
await live_stream.wait_sync_task
|
||||
if sync_future := get_instance(hass).async_get_commit_future():
|
||||
# Set the future so we can cancel it if the client
|
||||
# unsubscribes before the commit is done so we don't
|
||||
# query the database needlessly
|
||||
live_stream.wait_sync_future = sync_future
|
||||
await live_stream.wait_sync_future
|
||||
|
||||
#
|
||||
# Fetch any events from the database that have
|
||||
|
@ -169,6 +169,8 @@ browse_media:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
supported_features:
|
||||
- media_player.MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
fields:
|
||||
media_content_type:
|
||||
required: false
|
||||
|
@ -154,9 +154,8 @@ class OpenAIOptionsFlow(OptionsFlow):
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if user_input[CONF_LLM_HASS_API] == "none":
|
||||
user_input.pop(CONF_LLM_HASS_API)
|
||||
|
||||
if not user_input.get(CONF_LLM_HASS_API):
|
||||
user_input.pop(CONF_LLM_HASS_API, None)
|
||||
if user_input.get(CONF_CHAT_MODEL) in UNSUPPORTED_MODELS:
|
||||
errors[CONF_CHAT_MODEL] = "model_not_supported"
|
||||
|
||||
@ -178,7 +177,7 @@ class OpenAIOptionsFlow(OptionsFlow):
|
||||
options = {
|
||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
||||
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
|
||||
CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API),
|
||||
}
|
||||
|
||||
schema = openai_config_option_schema(self.hass, options)
|
||||
@ -248,19 +247,16 @@ def openai_config_option_schema(
|
||||
) -> VolDictType:
|
||||
"""Return a schema for OpenAI completion options."""
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
SelectOptionDict(
|
||||
label="No control",
|
||||
value="none",
|
||||
)
|
||||
]
|
||||
hass_apis.extend(
|
||||
SelectOptionDict(
|
||||
label=api.name,
|
||||
value=api.id,
|
||||
)
|
||||
for api in llm.async_get_apis(hass)
|
||||
)
|
||||
|
||||
]
|
||||
if (suggested_llm_apis := options.get(CONF_LLM_HASS_API)) and isinstance(
|
||||
suggested_llm_apis, str
|
||||
):
|
||||
suggested_llm_apis = [suggested_llm_apis]
|
||||
schema: VolDictType = {
|
||||
vol.Optional(
|
||||
CONF_PROMPT,
|
||||
@ -272,9 +268,8 @@ def openai_config_option_schema(
|
||||
): TemplateSelector(),
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
description={"suggested_value": options.get(CONF_LLM_HASS_API)},
|
||||
default="none",
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis)),
|
||||
description={"suggested_value": suggested_llm_apis},
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
|
||||
vol.Required(
|
||||
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
|
||||
): bool,
|
||||
|
@ -49,6 +49,7 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
|
||||
key=OverkizState.CORE_WATER_DETECTION,
|
||||
name="Water",
|
||||
icon="mdi:water",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
value_fn=lambda state: state == OverkizCommandParam.DETECTED,
|
||||
),
|
||||
# AirSensor/AirFlowSensor
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory, UnitOfTemperature
|
||||
from homeassistant.const import EntityCategory, UnitOfTemperature, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@ -172,6 +172,8 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [
|
||||
native_max_value=7,
|
||||
set_native_value=_async_set_native_value_boost_mode_duration,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
),
|
||||
# DomesticHotWaterProduction - away mode in days (0 - 6)
|
||||
OverkizNumberDescription(
|
||||
@ -182,6 +184,8 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [
|
||||
native_min_value=0,
|
||||
native_max_value=6,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -23,6 +23,7 @@ from homeassistant.const import (
|
||||
EntityCategory,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
UnitOfVolume,
|
||||
@ -126,6 +127,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
name="Outlet engine",
|
||||
icon="mdi:fan-chevron-down",
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
@ -152,14 +154,23 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_FOSSIL_ENERGY_CONSUMPTION,
|
||||
name="Fossil energy consumption",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_GAS_CONSUMPTION,
|
||||
name="Gas consumption",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_THERMAL_ENERGY_CONSUMPTION,
|
||||
name="Thermal energy consumption",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
# LightSensor/LuminanceSensor
|
||||
OverkizSensorDescription(
|
||||
@ -204,7 +215,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
# core:MeasuredValueType = core:ElectricalEnergyInWh
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF2,
|
||||
@ -213,7 +224,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
# core:MeasuredValueType = core:ElectricalEnergyInWh
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF3,
|
||||
@ -222,7 +233,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
# core:MeasuredValueType = core:ElectricalEnergyInWh
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF4,
|
||||
@ -231,7 +242,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
# core:MeasuredValueType = core:ElectricalEnergyInWh
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF5,
|
||||
@ -240,7 +251,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
# core:MeasuredValueType = core:ElectricalEnergyInWh
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF6,
|
||||
@ -249,7 +260,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
# core:MeasuredValueType = core:ElectricalEnergyInWh
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF7,
|
||||
@ -258,7 +269,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
# core:MeasuredValueType = core:ElectricalEnergyInWh
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF8,
|
||||
@ -267,7 +278,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
# core:MeasuredValueType = core:ElectricalEnergyInWh
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF9,
|
||||
@ -276,7 +287,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
# core:MeasuredValueType = core:ElectricalEnergyInWh
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
# HumiditySensor/RelativeHumiditySensor
|
||||
OverkizSensorDescription(
|
||||
@ -342,6 +353,8 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
name="Sun energy",
|
||||
native_value=lambda value: round(cast(float, value), 2),
|
||||
icon="mdi:solar-power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# WindSensor/WindSpeedSensor
|
||||
@ -350,6 +363,8 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
name="Wind speed",
|
||||
native_value=lambda value: round(cast(float, value), 2),
|
||||
icon="mdi:weather-windy",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# SmokeSensor/SmokeSensor
|
||||
@ -398,6 +413,8 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
native_value=lambda value: OVERKIZ_STATE_TO_TRANSLATION.get(
|
||||
cast(str, value), cast(str, value)
|
||||
),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["dead", "low_battery", "maintenance_required", "no_defect"],
|
||||
),
|
||||
# DomesticHotWaterProduction/WaterHeatingSystem
|
||||
OverkizSensorDescription(
|
||||
|
@ -1307,11 +1307,17 @@ class Recorder(threading.Thread):
|
||||
|
||||
async def async_block_till_done(self) -> None:
|
||||
"""Async version of block_till_done."""
|
||||
if future := self.async_get_commit_future():
|
||||
await future
|
||||
|
||||
@callback
|
||||
def async_get_commit_future(self) -> asyncio.Future[None] | None:
|
||||
"""Return a future that will wait for the next commit or None if nothing pending."""
|
||||
if self._queue.empty() and not self._event_session_has_pending_writes:
|
||||
return
|
||||
event = asyncio.Event()
|
||||
self.queue_task(SynchronizeTask(event))
|
||||
await event.wait()
|
||||
return None
|
||||
future: asyncio.Future[None] = self.hass.loop.create_future()
|
||||
self.queue_task(SynchronizeTask(future))
|
||||
return future
|
||||
|
||||
def block_till_done(self) -> None:
|
||||
"""Block till all events processed.
|
||||
|
@ -317,13 +317,18 @@ class SynchronizeTask(RecorderTask):
|
||||
"""Ensure all pending data has been committed."""
|
||||
|
||||
# commit_before is the default
|
||||
event: asyncio.Event
|
||||
future: asyncio.Future
|
||||
|
||||
def run(self, instance: Recorder) -> None:
|
||||
"""Handle the task."""
|
||||
# Does not use a tracked task to avoid
|
||||
# blocking shutdown if the recorder is broken
|
||||
instance.hass.loop.call_soon_threadsafe(self.event.set)
|
||||
instance.hass.loop.call_soon_threadsafe(self._set_result_if_not_done)
|
||||
|
||||
def _set_result_if_not_done(self) -> None:
|
||||
"""Set the result if not done."""
|
||||
if not self.future.done():
|
||||
self.future.set_result(None)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
@ -7,7 +7,9 @@ DOMAIN = "renault"
|
||||
CONF_LOCALE = "locale"
|
||||
CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id"
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = 420 # 7 minutes
|
||||
# normal number of allowed calls per hour to the API
|
||||
# for a single car and the 7 coordinator, it is a scan every 7mn
|
||||
MAX_CALLS_PER_HOURS = 60
|
||||
|
||||
# If throttled time to pause the updates, in seconds
|
||||
COOLING_UPDATES_SECONDS = 60 * 15 # 15 minutes
|
||||
|
@ -32,9 +32,9 @@ from time import time
|
||||
from .const import (
|
||||
CONF_KAMEREON_ACCOUNT_ID,
|
||||
COOLING_UPDATES_SECONDS,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
MAX_CALLS_PER_HOURS,
|
||||
)
|
||||
from .renault_vehicle import RenaultVehicleProxy
|
||||
from .renault_vehicle import COORDINATORS, RenaultVehicleProxy
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -82,7 +82,6 @@ class RenaultHub:
|
||||
async def async_initialise(self, config_entry: RenaultConfigEntry) -> None:
|
||||
"""Set up proxy."""
|
||||
account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID]
|
||||
scan_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
self._account = await self._client.get_api_account(account_id)
|
||||
vehicles = await self._account.get_vehicles()
|
||||
@ -94,6 +93,12 @@ class RenaultHub:
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to retrieve vehicle details from Renault servers"
|
||||
)
|
||||
|
||||
num_call_per_scan = len(COORDINATORS) * len(vehicles.vehicleLinks)
|
||||
scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
)
|
||||
|
||||
device_registry = dr.async_get(self._hass)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
@ -108,6 +113,21 @@ class RenaultHub:
|
||||
)
|
||||
)
|
||||
|
||||
# all vehicles have been initiated with the right number of active coordinators
|
||||
num_call_per_scan = 0
|
||||
for vehicle_link in vehicles.vehicleLinks:
|
||||
vehicle = self._vehicles[str(vehicle_link.vin)]
|
||||
num_call_per_scan += len(vehicle.coordinators)
|
||||
|
||||
new_scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
)
|
||||
if new_scan_interval != scan_interval:
|
||||
# we need to change the vehicles with the right scan interval
|
||||
for vehicle_link in vehicles.vehicleLinks:
|
||||
vehicle = self._vehicles[str(vehicle_link.vin)]
|
||||
vehicle.update_scan_interval(new_scan_interval)
|
||||
|
||||
async def async_initialise_vehicle(
|
||||
self,
|
||||
vehicle_link: KamereonVehiclesLink,
|
||||
|
@ -91,6 +91,13 @@ class RenaultVehicleProxy:
|
||||
self._scan_interval = scan_interval
|
||||
self._hub = hub
|
||||
|
||||
def update_scan_interval(self, scan_interval: timedelta) -> None:
|
||||
"""Set the scan interval for the vehicle."""
|
||||
if scan_interval != self._scan_interval:
|
||||
self._scan_interval = scan_interval
|
||||
for coordinator in self.coordinators.values():
|
||||
coordinator.update_interval = scan_interval
|
||||
|
||||
@property
|
||||
def details(self) -> models.KamereonVehicleDetails:
|
||||
"""Return the specs of the vehicle."""
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@ -105,91 +104,96 @@ SERVICES = [
|
||||
]
|
||||
|
||||
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the Renault services."""
|
||||
async def ac_cancel(service_call: ServiceCall) -> None:
|
||||
"""Cancel A/C."""
|
||||
proxy = get_vehicle_proxy(service_call)
|
||||
|
||||
async def ac_cancel(service_call: ServiceCall) -> None:
|
||||
"""Cancel A/C."""
|
||||
proxy = get_vehicle_proxy(service_call.data)
|
||||
LOGGER.debug("A/C cancel attempt")
|
||||
result = await proxy.set_ac_stop()
|
||||
LOGGER.debug("A/C cancel result: %s", result)
|
||||
|
||||
LOGGER.debug("A/C cancel attempt")
|
||||
result = await proxy.set_ac_stop()
|
||||
LOGGER.debug("A/C cancel result: %s", result)
|
||||
|
||||
async def ac_start(service_call: ServiceCall) -> None:
|
||||
"""Start A/C."""
|
||||
temperature: float = service_call.data[ATTR_TEMPERATURE]
|
||||
when: datetime | None = service_call.data.get(ATTR_WHEN)
|
||||
proxy = get_vehicle_proxy(service_call.data)
|
||||
async def ac_start(service_call: ServiceCall) -> None:
|
||||
"""Start A/C."""
|
||||
temperature: float = service_call.data[ATTR_TEMPERATURE]
|
||||
when: datetime | None = service_call.data.get(ATTR_WHEN)
|
||||
proxy = get_vehicle_proxy(service_call)
|
||||
|
||||
LOGGER.debug("A/C start attempt: %s / %s", temperature, when)
|
||||
result = await proxy.set_ac_start(temperature, when)
|
||||
LOGGER.debug("A/C start result: %s", result.raw_data)
|
||||
LOGGER.debug("A/C start attempt: %s / %s", temperature, when)
|
||||
result = await proxy.set_ac_start(temperature, when)
|
||||
LOGGER.debug("A/C start result: %s", result.raw_data)
|
||||
|
||||
async def charge_set_schedules(service_call: ServiceCall) -> None:
|
||||
"""Set charge schedules."""
|
||||
schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES]
|
||||
proxy = get_vehicle_proxy(service_call.data)
|
||||
charge_schedules = await proxy.get_charging_settings()
|
||||
for schedule in schedules:
|
||||
charge_schedules.update(schedule)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert charge_schedules.schedules is not None
|
||||
LOGGER.debug("Charge set schedules attempt: %s", schedules)
|
||||
result = await proxy.set_charge_schedules(charge_schedules.schedules)
|
||||
async def charge_set_schedules(service_call: ServiceCall) -> None:
|
||||
"""Set charge schedules."""
|
||||
schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES]
|
||||
proxy = get_vehicle_proxy(service_call)
|
||||
charge_schedules = await proxy.get_charging_settings()
|
||||
for schedule in schedules:
|
||||
charge_schedules.update(schedule)
|
||||
|
||||
LOGGER.debug("Charge set schedules result: %s", result)
|
||||
LOGGER.debug(
|
||||
"It may take some time before these changes are reflected in your vehicle"
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
assert charge_schedules.schedules is not None
|
||||
LOGGER.debug("Charge set schedules attempt: %s", schedules)
|
||||
result = await proxy.set_charge_schedules(charge_schedules.schedules)
|
||||
|
||||
async def ac_set_schedules(service_call: ServiceCall) -> None:
|
||||
"""Set A/C schedules."""
|
||||
schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES]
|
||||
proxy = get_vehicle_proxy(service_call.data)
|
||||
hvac_schedules = await proxy.get_hvac_settings()
|
||||
LOGGER.debug("Charge set schedules result: %s", result)
|
||||
LOGGER.debug(
|
||||
"It may take some time before these changes are reflected in your vehicle"
|
||||
)
|
||||
|
||||
for schedule in schedules:
|
||||
hvac_schedules.update(schedule)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert hvac_schedules.schedules is not None
|
||||
LOGGER.debug("HVAC set schedules attempt: %s", schedules)
|
||||
result = await proxy.set_hvac_schedules(hvac_schedules.schedules)
|
||||
async def ac_set_schedules(service_call: ServiceCall) -> None:
|
||||
"""Set A/C schedules."""
|
||||
schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES]
|
||||
proxy = get_vehicle_proxy(service_call)
|
||||
hvac_schedules = await proxy.get_hvac_settings()
|
||||
|
||||
LOGGER.debug("HVAC set schedules result: %s", result)
|
||||
LOGGER.debug(
|
||||
"It may take some time before these changes are reflected in your vehicle"
|
||||
)
|
||||
for schedule in schedules:
|
||||
hvac_schedules.update(schedule)
|
||||
|
||||
def get_vehicle_proxy(service_call_data: Mapping) -> RenaultVehicleProxy:
|
||||
"""Get vehicle from service_call data."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_id = service_call_data[ATTR_VEHICLE]
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
if device_entry is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device_id",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
assert hvac_schedules.schedules is not None
|
||||
LOGGER.debug("HVAC set schedules attempt: %s", schedules)
|
||||
result = await proxy.set_hvac_schedules(hvac_schedules.schedules)
|
||||
|
||||
loaded_entries: list[RenaultConfigEntry] = [
|
||||
entry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
if entry.entry_id in device_entry.config_entries
|
||||
]
|
||||
for entry in loaded_entries:
|
||||
for vin, vehicle in entry.runtime_data.vehicles.items():
|
||||
if (DOMAIN, vin) in device_entry.identifiers:
|
||||
return vehicle
|
||||
LOGGER.debug("HVAC set schedules result: %s", result)
|
||||
LOGGER.debug(
|
||||
"It may take some time before these changes are reflected in your vehicle"
|
||||
)
|
||||
|
||||
|
||||
def get_vehicle_proxy(service_call: ServiceCall) -> RenaultVehicleProxy:
|
||||
"""Get vehicle from service_call data."""
|
||||
device_registry = dr.async_get(service_call.hass)
|
||||
device_id = service_call.data[ATTR_VEHICLE]
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
if device_entry is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_config_entry_for_device",
|
||||
translation_placeholders={"device_id": device_entry.name or device_id},
|
||||
translation_key="invalid_device_id",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
loaded_entries: list[RenaultConfigEntry] = [
|
||||
entry
|
||||
for entry in service_call.hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
if entry.entry_id in device_entry.config_entries
|
||||
]
|
||||
for entry in loaded_entries:
|
||||
for vin, vehicle in entry.runtime_data.vehicles.items():
|
||||
if (DOMAIN, vin) in device_entry.identifiers:
|
||||
return vehicle
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_config_entry_for_device",
|
||||
translation_placeholders={"device_id": device_entry.name or device_id},
|
||||
)
|
||||
|
||||
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the Renault services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_AC_CANCEL,
|
||||
|
@ -59,10 +59,11 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
Category.DOOR: BinarySensorDeviceClass.DOOR,
|
||||
Category.WINDOW: BinarySensorDeviceClass.WINDOW,
|
||||
},
|
||||
exists_fn=lambda key: key in {"freezer", "cooler"},
|
||||
exists_fn=lambda key: key in {"freezer", "cooler", "cvroom"},
|
||||
component_translation_key={
|
||||
"freezer": "freezer_door",
|
||||
"cooler": "cooler_door",
|
||||
"cvroom": "cool_select_plus_door",
|
||||
},
|
||||
deprecated_fn=(
|
||||
lambda status: "fridge_door"
|
||||
|
@ -23,7 +23,6 @@ from .entity import SmartThingsEntity
|
||||
MEDIA_PLAYER_CAPABILITIES = (
|
||||
Capability.AUDIO_MUTE,
|
||||
Capability.AUDIO_VOLUME,
|
||||
Capability.MEDIA_PLAYBACK,
|
||||
)
|
||||
|
||||
CONTROLLABLE_SOURCES = ["bluetooth", "wifi"]
|
||||
@ -100,27 +99,25 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
|
||||
)
|
||||
|
||||
def _determine_features(self) -> MediaPlayerEntityFeature:
|
||||
flags = MediaPlayerEntityFeature(0)
|
||||
playback_commands = self.get_attribute_value(
|
||||
Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS
|
||||
flags = (
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
)
|
||||
if "play" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.PLAY
|
||||
if "pause" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.PAUSE
|
||||
if "stop" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.STOP
|
||||
if "rewind" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
if "fastForward" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.NEXT_TRACK
|
||||
if self.supports_capability(Capability.AUDIO_VOLUME):
|
||||
flags |= (
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
if self.supports_capability(Capability.MEDIA_PLAYBACK):
|
||||
playback_commands = self.get_attribute_value(
|
||||
Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS
|
||||
)
|
||||
if self.supports_capability(Capability.AUDIO_MUTE):
|
||||
flags |= MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
if "play" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.PLAY
|
||||
if "pause" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.PAUSE
|
||||
if "stop" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.STOP
|
||||
if "rewind" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
if "fastForward" in playback_commands:
|
||||
flags |= MediaPlayerEntityFeature.NEXT_TRACK
|
||||
if self.supports_capability(Capability.SWITCH):
|
||||
flags |= (
|
||||
MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF
|
||||
@ -270,6 +267,13 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""State of the media player."""
|
||||
if self.supports_capability(Capability.SWITCH):
|
||||
if not self.supports_capability(Capability.MEDIA_PLAYBACK):
|
||||
if (
|
||||
self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
|
||||
== "on"
|
||||
):
|
||||
return MediaPlayerState.ON
|
||||
return MediaPlayerState.OFF
|
||||
if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on":
|
||||
if (
|
||||
self.source is not None
|
||||
|
@ -194,13 +194,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
deprecated=(
|
||||
lambda status: "media_player"
|
||||
if all(
|
||||
capability in status
|
||||
for capability in (
|
||||
Capability.AUDIO_MUTE,
|
||||
Capability.MEDIA_PLAYBACK,
|
||||
)
|
||||
)
|
||||
if Capability.AUDIO_MUTE in status
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
@ -48,6 +48,9 @@
|
||||
"cooler_door": {
|
||||
"name": "Cooler door"
|
||||
},
|
||||
"cool_select_plus_door": {
|
||||
"name": "CoolSelect+ door"
|
||||
},
|
||||
"remote_control": {
|
||||
"name": "Remote control"
|
||||
},
|
||||
|
@ -38,7 +38,6 @@ AC_CAPABILITIES = (
|
||||
MEDIA_PLAYER_CAPABILITIES = (
|
||||
Capability.AUDIO_MUTE,
|
||||
Capability.AUDIO_VOLUME,
|
||||
Capability.MEDIA_PLAYBACK,
|
||||
)
|
||||
|
||||
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smhi",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pysmhi"],
|
||||
"requirements": ["pysmhi==1.0.1"]
|
||||
"requirements": ["pysmhi==1.0.2"]
|
||||
}
|
||||
|
@ -6,8 +6,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from teslemetry_stream import Signal
|
||||
from teslemetry_stream.const import WindowState
|
||||
from teslemetry_stream.vehicle import TeslemetryStreamVehicle
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@ -32,6 +31,12 @@ from .models import TeslemetryEnergyData, TeslemetryVehicleData
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
WINDOW_STATES = {
|
||||
"Opened": True,
|
||||
"PartiallyOpen": True,
|
||||
"Closed": False,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TeslemetryBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
@ -39,11 +44,14 @@ class TeslemetryBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
|
||||
polling_value_fn: Callable[[StateType], bool | None] = bool
|
||||
polling: bool = False
|
||||
streaming_key: Signal | None = None
|
||||
streaming_listener: (
|
||||
Callable[
|
||||
[TeslemetryStreamVehicle, Callable[[bool | None], None]],
|
||||
Callable[[], None],
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
streaming_firmware: str = "2024.26"
|
||||
streaming_value_fn: Callable[[StateType], bool | None] = (
|
||||
lambda x: x is True or x == "true"
|
||||
)
|
||||
|
||||
|
||||
VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||
@ -56,7 +64,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="charge_state_battery_heater_on",
|
||||
polling=True,
|
||||
streaming_key=Signal.BATTERY_HEATER_ON,
|
||||
streaming_listener=lambda x, y: x.listen_BatteryHeaterOn(y),
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
@ -64,15 +72,16 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="charge_state_charger_phases",
|
||||
polling=True,
|
||||
streaming_key=Signal.CHARGER_PHASES,
|
||||
streaming_listener=lambda x, y: x.listen_ChargerPhases(
|
||||
lambda z: y(None if z is None else z > 1)
|
||||
),
|
||||
polling_value_fn=lambda x: cast(int, x) > 1,
|
||||
streaming_value_fn=lambda x: cast(int, x) > 1,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="charge_state_preconditioning_enabled",
|
||||
polling=True,
|
||||
streaming_key=Signal.PRECONDITIONING_ENABLED,
|
||||
streaming_listener=lambda x, y: x.listen_PreconditioningEnabled(y),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@ -85,7 +94,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="charge_state_scheduled_charging_pending",
|
||||
polling=True,
|
||||
streaming_key=Signal.SCHEDULED_CHARGING_PENDING,
|
||||
streaming_listener=lambda x, y: x.listen_ScheduledChargingPending(y),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@ -153,32 +162,36 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="vehicle_state_fd_window",
|
||||
polling=True,
|
||||
streaming_key=Signal.FD_WINDOW,
|
||||
streaming_value_fn=lambda x: WindowState.get(x) != "Closed",
|
||||
streaming_listener=lambda x, y: x.listen_FrontDriverWindow(
|
||||
lambda z: y(WINDOW_STATES.get(z))
|
||||
),
|
||||
device_class=BinarySensorDeviceClass.WINDOW,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="vehicle_state_fp_window",
|
||||
polling=True,
|
||||
streaming_key=Signal.FP_WINDOW,
|
||||
streaming_value_fn=lambda x: WindowState.get(x) != "Closed",
|
||||
streaming_listener=lambda x, y: x.listen_FrontPassengerWindow(
|
||||
lambda z: y(WINDOW_STATES.get(z))
|
||||
),
|
||||
device_class=BinarySensorDeviceClass.WINDOW,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="vehicle_state_rd_window",
|
||||
polling=True,
|
||||
streaming_key=Signal.RD_WINDOW,
|
||||
streaming_value_fn=lambda x: WindowState.get(x) != "Closed",
|
||||
streaming_listener=lambda x, y: x.listen_RearDriverWindow(
|
||||
lambda z: y(WINDOW_STATES.get(z))
|
||||
),
|
||||
device_class=BinarySensorDeviceClass.WINDOW,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="vehicle_state_rp_window",
|
||||
polling=True,
|
||||
streaming_key=Signal.RP_WINDOW,
|
||||
streaming_value_fn=lambda x: WindowState.get(x) != "Closed",
|
||||
streaming_listener=lambda x, y: x.listen_RearPassengerWindow(
|
||||
lambda z: y(WINDOW_STATES.get(z))
|
||||
),
|
||||
device_class=BinarySensorDeviceClass.WINDOW,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
@ -186,180 +199,177 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||
key="vehicle_state_df",
|
||||
polling=True,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
streaming_key=Signal.DOOR_STATE,
|
||||
streaming_value_fn=lambda x: cast(dict, x).get("DriverFront"),
|
||||
streaming_listener=lambda x, y: x.listen_FrontDriverDoor(y),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="vehicle_state_dr",
|
||||
polling=True,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
streaming_key=Signal.DOOR_STATE,
|
||||
streaming_value_fn=lambda x: cast(dict, x).get("DriverRear"),
|
||||
streaming_listener=lambda x, y: x.listen_RearDriverDoor(y),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="vehicle_state_pf",
|
||||
polling=True,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
streaming_key=Signal.DOOR_STATE,
|
||||
streaming_value_fn=lambda x: cast(dict, x).get("PassengerFront"),
|
||||
streaming_listener=lambda x, y: x.listen_FrontPassengerDoor(y),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="vehicle_state_pr",
|
||||
polling=True,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
streaming_key=Signal.DOOR_STATE,
|
||||
streaming_value_fn=lambda x: cast(dict, x).get("PassengerRear"),
|
||||
streaming_listener=lambda x, y: x.listen_RearPassengerDoor(y),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="automatic_blind_spot_camera",
|
||||
streaming_key=Signal.AUTOMATIC_BLIND_SPOT_CAMERA,
|
||||
streaming_listener=lambda x, y: x.listen_AutomaticBlindSpotCamera(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="automatic_emergency_braking_off",
|
||||
streaming_key=Signal.AUTOMATIC_EMERGENCY_BRAKING_OFF,
|
||||
streaming_listener=lambda x, y: x.listen_AutomaticEmergencyBrakingOff(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="blind_spot_collision_warning_chime",
|
||||
streaming_key=Signal.BLIND_SPOT_COLLISION_WARNING_CHIME,
|
||||
streaming_listener=lambda x, y: x.listen_BlindSpotCollisionWarningChime(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="bms_full_charge_complete",
|
||||
streaming_key=Signal.BMS_FULL_CHARGE_COMPLETE,
|
||||
streaming_listener=lambda x, y: x.listen_BmsFullchargecomplete(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="brake_pedal",
|
||||
streaming_key=Signal.BRAKE_PEDAL,
|
||||
streaming_listener=lambda x, y: x.listen_BrakePedal(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="charge_port_cold_weather_mode",
|
||||
streaming_key=Signal.CHARGE_PORT_COLD_WEATHER_MODE,
|
||||
streaming_listener=lambda x, y: x.listen_ChargePortColdWeatherMode(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="service_mode",
|
||||
streaming_key=Signal.SERVICE_MODE,
|
||||
streaming_listener=lambda x, y: x.listen_ServiceMode(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="pin_to_drive_enabled",
|
||||
streaming_key=Signal.PIN_TO_DRIVE_ENABLED,
|
||||
streaming_listener=lambda x, y: x.listen_PinToDriveEnabled(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="drive_rail",
|
||||
streaming_key=Signal.DRIVE_RAIL,
|
||||
streaming_listener=lambda x, y: x.listen_DriveRail(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="driver_seat_belt",
|
||||
streaming_key=Signal.DRIVER_SEAT_BELT,
|
||||
streaming_listener=lambda x, y: x.listen_DriverSeatBelt(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="driver_seat_occupied",
|
||||
streaming_key=Signal.DRIVER_SEAT_OCCUPIED,
|
||||
streaming_listener=lambda x, y: x.listen_DriverSeatOccupied(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="passenger_seat_belt",
|
||||
streaming_key=Signal.PASSENGER_SEAT_BELT,
|
||||
streaming_listener=lambda x, y: x.listen_PassengerSeatBelt(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="fast_charger_present",
|
||||
streaming_key=Signal.FAST_CHARGER_PRESENT,
|
||||
streaming_listener=lambda x, y: x.listen_FastChargerPresent(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="gps_state",
|
||||
streaming_key=Signal.GPS_STATE,
|
||||
streaming_listener=lambda x, y: x.listen_GpsState(y),
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="guest_mode_enabled",
|
||||
streaming_key=Signal.GUEST_MODE_ENABLED,
|
||||
streaming_listener=lambda x, y: x.listen_GuestModeEnabled(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="dc_dc_enable",
|
||||
streaming_key=Signal.DCDC_ENABLE,
|
||||
streaming_listener=lambda x, y: x.listen_DCDCEnable(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="emergency_lane_departure_avoidance",
|
||||
streaming_key=Signal.EMERGENCY_LANE_DEPARTURE_AVOIDANCE,
|
||||
streaming_listener=lambda x, y: x.listen_EmergencyLaneDepartureAvoidance(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="supercharger_session_trip_planner",
|
||||
streaming_key=Signal.SUPERCHARGER_SESSION_TRIP_PLANNER,
|
||||
streaming_listener=lambda x, y: x.listen_SuperchargerSessionTripPlanner(y),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="wiper_heat_enabled",
|
||||
streaming_key=Signal.WIPER_HEAT_ENABLED,
|
||||
streaming_listener=lambda x, y: x.listen_WiperHeatEnabled(y),
|
||||
streaming_firmware="2024.44.25",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="rear_display_hvac_enabled",
|
||||
streaming_key=Signal.REAR_DISPLAY_HVAC_ENABLED,
|
||||
streaming_listener=lambda x, y: x.listen_RearDisplayHvacEnabled(y),
|
||||
streaming_firmware="2024.44.25",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="offroad_lightbar_present",
|
||||
streaming_key=Signal.OFFROAD_LIGHTBAR_PRESENT,
|
||||
streaming_listener=lambda x, y: x.listen_OffroadLightbarPresent(y),
|
||||
streaming_firmware="2024.44.25",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="homelink_nearby",
|
||||
streaming_key=Signal.HOMELINK_NEARBY,
|
||||
streaming_listener=lambda x, y: x.listen_HomelinkNearby(y),
|
||||
streaming_firmware="2024.44.25",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="europe_vehicle",
|
||||
streaming_key=Signal.EUROPE_VEHICLE,
|
||||
streaming_listener=lambda x, y: x.listen_EuropeVehicle(y),
|
||||
streaming_firmware="2024.44.25",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="right_hand_drive",
|
||||
streaming_key=Signal.RIGHT_HAND_DRIVE,
|
||||
streaming_listener=lambda x, y: x.listen_RightHandDrive(y),
|
||||
streaming_firmware="2024.44.25",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="located_at_home",
|
||||
streaming_key=Signal.LOCATED_AT_HOME,
|
||||
streaming_listener=lambda x, y: x.listen_LocatedAtHome(y),
|
||||
streaming_firmware="2024.44.32",
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="located_at_work",
|
||||
streaming_key=Signal.LOCATED_AT_WORK,
|
||||
streaming_listener=lambda x, y: x.listen_LocatedAtWork(y),
|
||||
streaming_firmware="2024.44.32",
|
||||
),
|
||||
TeslemetryBinarySensorEntityDescription(
|
||||
key="located_at_favorite",
|
||||
streaming_key=Signal.LOCATED_AT_FAVORITE,
|
||||
streaming_listener=lambda x, y: x.listen_LocatedAtFavorite(y),
|
||||
streaming_firmware="2024.44.32",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
ENERGY_LIVE_DESCRIPTIONS: tuple[BinarySensorEntityDescription, ...] = (
|
||||
BinarySensorEntityDescription(key="backup_capable"),
|
||||
BinarySensorEntityDescription(key="grid_services_active"),
|
||||
@ -386,7 +396,7 @@ async def async_setup_entry(
|
||||
for description in VEHICLE_DESCRIPTIONS:
|
||||
if (
|
||||
not vehicle.api.pre2021
|
||||
and description.streaming_key
|
||||
and description.streaming_listener
|
||||
and vehicle.firmware >= description.streaming_firmware
|
||||
):
|
||||
entities.append(
|
||||
@ -453,8 +463,7 @@ class TeslemetryVehicleStreamingBinarySensorEntity(
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
assert description.streaming_key
|
||||
super().__init__(data, description.key, description.streaming_key)
|
||||
super().__init__(data, description.key)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
@ -462,11 +471,18 @@ class TeslemetryVehicleStreamingBinarySensorEntity(
|
||||
if (state := await self.async_get_last_state()) is not None:
|
||||
self._attr_is_on = state.state == STATE_ON
|
||||
|
||||
def _async_value_from_stream(self, value) -> None:
|
||||
assert self.entity_description.streaming_listener
|
||||
self.async_on_remove(
|
||||
self.entity_description.streaming_listener(
|
||||
self.vehicle.stream_vehicle, self._async_value_from_stream
|
||||
)
|
||||
)
|
||||
|
||||
def _async_value_from_stream(self, value: bool | None) -> None:
|
||||
"""Update the value of the entity."""
|
||||
self._attr_available = value is not None
|
||||
if self._attr_available:
|
||||
self._attr_is_on = self.entity_description.streaming_value_fn(value)
|
||||
self._attr_is_on = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class TeslemetryEnergyLiveBinarySensorEntity(
|
||||
|
@ -7,8 +7,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from propcache.api import cached_property
|
||||
from teslemetry_stream import Signal, TeslemetryStreamVehicle
|
||||
from teslemetry_stream.const import ShiftState
|
||||
from teslemetry_stream import TeslemetryStreamVehicle
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
RestoreSensor,
|
||||
@ -70,8 +69,13 @@ class TeslemetryVehicleSensorEntityDescription(SensorEntityDescription):
|
||||
polling: bool = False
|
||||
polling_value_fn: Callable[[StateType], StateType] = lambda x: x
|
||||
nullable: bool = False
|
||||
streaming_key: Signal | None = None
|
||||
streaming_value_fn: Callable[[str | int | float], StateType] = lambda x: x
|
||||
streaming_listener: (
|
||||
Callable[
|
||||
[TeslemetryStreamVehicle, Callable[[StateType], None]],
|
||||
Callable[[], None],
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
streaming_firmware: str = "2024.26"
|
||||
|
||||
|
||||
@ -79,18 +83,17 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_charging_state",
|
||||
polling=True,
|
||||
streaming_key=Signal.DETAILED_CHARGE_STATE,
|
||||
polling_value_fn=lambda value: CHARGE_STATES.get(str(value)),
|
||||
streaming_value_fn=lambda value: CHARGE_STATES.get(
|
||||
str(value).replace("DetailedChargeState", "")
|
||||
streaming_listener=lambda x, y: x.listen_DetailedChargeState(
|
||||
lambda z: None if z is None else y(z.lower())
|
||||
),
|
||||
polling_value_fn=lambda value: CHARGE_STATES.get(str(value)),
|
||||
options=list(CHARGE_STATES.values()),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
),
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_battery_level",
|
||||
polling=True,
|
||||
streaming_key=Signal.BATTERY_LEVEL,
|
||||
streaming_listener=lambda x, y: x.listen_BatteryLevel(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
@ -99,15 +102,17 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_usable_battery_level",
|
||||
polling=True,
|
||||
streaming_listener=lambda x, y: x.listen_Soc(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_registry_enabled_default=False,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_charge_energy_added",
|
||||
polling=True,
|
||||
streaming_key=Signal.AC_CHARGING_ENERGY_IN,
|
||||
streaming_listener=lambda x, y: x.listen_ACChargingEnergyIn(y),
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
@ -116,7 +121,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_charger_power",
|
||||
polling=True,
|
||||
streaming_key=Signal.AC_CHARGING_POWER,
|
||||
streaming_listener=lambda x, y: x.listen_ACChargingPower(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
@ -124,7 +129,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_charger_voltage",
|
||||
polling=True,
|
||||
streaming_key=Signal.CHARGER_VOLTAGE,
|
||||
streaming_listener=lambda x, y: x.listen_ChargerVoltage(y),
|
||||
streaming_firmware="2024.44.32",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
@ -134,7 +139,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_charger_actual_current",
|
||||
polling=True,
|
||||
streaming_key=Signal.CHARGE_AMPS,
|
||||
streaming_listener=lambda x, y: x.listen_ChargeAmps(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
@ -151,14 +156,14 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_conn_charge_cable",
|
||||
polling=True,
|
||||
streaming_key=Signal.CHARGING_CABLE_TYPE,
|
||||
streaming_listener=lambda x, y: x.listen_ChargingCableType(y),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_fast_charger_type",
|
||||
polling=True,
|
||||
streaming_key=Signal.FAST_CHARGER_TYPE,
|
||||
streaming_listener=lambda x, y: x.listen_FastChargerType(y),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@ -173,7 +178,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_est_battery_range",
|
||||
polling=True,
|
||||
streaming_key=Signal.EST_BATTERY_RANGE,
|
||||
streaming_listener=lambda x, y: x.listen_EstBatteryRange(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfLength.MILES,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
@ -183,7 +188,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="charge_state_ideal_battery_range",
|
||||
polling=True,
|
||||
streaming_key=Signal.IDEAL_BATTERY_RANGE,
|
||||
streaming_listener=lambda x, y: x.listen_IdealBatteryRange(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfLength.MILES,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
@ -194,7 +199,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
key="drive_state_speed",
|
||||
polling=True,
|
||||
polling_value_fn=lambda value: value or 0,
|
||||
streaming_key=Signal.VEHICLE_SPEED,
|
||||
streaming_listener=lambda x, y: x.listen_VehicleSpeed(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.SPEED,
|
||||
@ -213,10 +218,11 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="drive_state_shift_state",
|
||||
polling=True,
|
||||
nullable=True,
|
||||
polling_value_fn=lambda x: SHIFT_STATES.get(str(x), "p"),
|
||||
streaming_key=Signal.GEAR,
|
||||
streaming_value_fn=lambda x: str(ShiftState.get(x, "P")).lower(),
|
||||
nullable=True,
|
||||
streaming_listener=lambda x, y: x.listen_Gear(
|
||||
lambda z: y("p" if z is None else z.lower())
|
||||
),
|
||||
options=list(SHIFT_STATES.values()),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_registry_enabled_default=False,
|
||||
@ -224,7 +230,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="vehicle_state_odometer",
|
||||
polling=True,
|
||||
streaming_key=Signal.ODOMETER,
|
||||
streaming_listener=lambda x, y: x.listen_Odometer(y),
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfLength.MILES,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
@ -235,7 +241,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="vehicle_state_tpms_pressure_fl",
|
||||
polling=True,
|
||||
streaming_key=Signal.TPMS_PRESSURE_FL,
|
||||
streaming_listener=lambda x, y: x.listen_TpmsPressureFl(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||
@ -247,7 +253,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="vehicle_state_tpms_pressure_fr",
|
||||
polling=True,
|
||||
streaming_key=Signal.TPMS_PRESSURE_FR,
|
||||
streaming_listener=lambda x, y: x.listen_TpmsPressureFr(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||
@ -259,7 +265,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="vehicle_state_tpms_pressure_rl",
|
||||
polling=True,
|
||||
streaming_key=Signal.TPMS_PRESSURE_RL,
|
||||
streaming_listener=lambda x, y: x.listen_TpmsPressureRl(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||
@ -271,7 +277,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="vehicle_state_tpms_pressure_rr",
|
||||
polling=True,
|
||||
streaming_key=Signal.TPMS_PRESSURE_RR,
|
||||
streaming_listener=lambda x, y: x.listen_TpmsPressureRr(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||
@ -283,7 +289,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="climate_state_inside_temp",
|
||||
polling=True,
|
||||
streaming_key=Signal.INSIDE_TEMP,
|
||||
streaming_listener=lambda x, y: x.listen_InsideTemp(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
@ -292,7 +298,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="climate_state_outside_temp",
|
||||
polling=True,
|
||||
streaming_key=Signal.OUTSIDE_TEMP,
|
||||
streaming_listener=lambda x, y: x.listen_OutsideTemp(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
@ -321,7 +327,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="drive_state_active_route_traffic_minutes_delay",
|
||||
polling=True,
|
||||
streaming_key=Signal.ROUTE_TRAFFIC_MINUTES_DELAY,
|
||||
streaming_listener=lambda x, y: x.listen_RouteTrafficMinutesDelay(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
@ -330,7 +336,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="drive_state_active_route_energy_at_arrival",
|
||||
polling=True,
|
||||
streaming_key=Signal.EXPECTED_ENERGY_PERCENT_AT_TRIP_ARRIVAL,
|
||||
streaming_listener=lambda x, y: x.listen_ExpectedEnergyPercentAtTripArrival(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
@ -340,7 +346,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="drive_state_active_route_miles_to_arrival",
|
||||
polling=True,
|
||||
streaming_key=Signal.MILES_TO_ARRIVAL,
|
||||
streaming_listener=lambda x, y: x.listen_MilesToArrival(y),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfLength.MILES,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
@ -358,14 +364,14 @@ class TeslemetryTimeEntityDescription(SensorEntityDescription):
|
||||
Callable[[], None],
|
||||
]
|
||||
streaming_firmware: str = "2024.26"
|
||||
streaming_value_fn: Callable[[float], float] = lambda x: x
|
||||
streaming_unit: str
|
||||
|
||||
|
||||
VEHICLE_TIME_DESCRIPTIONS: tuple[TeslemetryTimeEntityDescription, ...] = (
|
||||
TeslemetryTimeEntityDescription(
|
||||
key="charge_state_minutes_to_full_charge",
|
||||
streaming_value_fn=lambda x: x * 60,
|
||||
streaming_listener=lambda x, y: x.listen_TimeToFullCharge(y),
|
||||
streaming_unit="hours",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
variance=4,
|
||||
@ -373,6 +379,7 @@ VEHICLE_TIME_DESCRIPTIONS: tuple[TeslemetryTimeEntityDescription, ...] = (
|
||||
TeslemetryTimeEntityDescription(
|
||||
key="drive_state_active_route_minutes_to_arrival",
|
||||
streaming_listener=lambda x, y: x.listen_MinutesToArrival(y),
|
||||
streaming_unit="minutes",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
variance=1,
|
||||
),
|
||||
@ -547,7 +554,7 @@ async def async_setup_entry(
|
||||
for description in VEHICLE_DESCRIPTIONS:
|
||||
if (
|
||||
not vehicle.api.pre2021
|
||||
and description.streaming_key
|
||||
and description.streaming_listener
|
||||
and vehicle.firmware >= description.streaming_firmware
|
||||
):
|
||||
entities.append(TeslemetryStreamSensorEntity(vehicle, description))
|
||||
@ -613,8 +620,7 @@ class TeslemetryStreamSensorEntity(TeslemetryVehicleStreamEntity, RestoreSensor)
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
assert description.streaming_key
|
||||
super().__init__(data, description.key, description.streaming_key)
|
||||
super().__init__(data, description.key)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
@ -623,17 +629,22 @@ class TeslemetryStreamSensorEntity(TeslemetryVehicleStreamEntity, RestoreSensor)
|
||||
if (sensor_data := await self.async_get_last_sensor_data()) is not None:
|
||||
self._attr_native_value = sensor_data.native_value
|
||||
|
||||
if self.entity_description.streaming_listener is not None:
|
||||
self.async_on_remove(
|
||||
self.entity_description.streaming_listener(
|
||||
self.vehicle.stream_vehicle, self._async_value_from_stream
|
||||
)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self.stream.connected
|
||||
|
||||
def _async_value_from_stream(self, value) -> None:
|
||||
def _async_value_from_stream(self, value: StateType) -> None:
|
||||
"""Update the value of the entity."""
|
||||
if self.entity_description.nullable or value is not None:
|
||||
self._attr_native_value = self.entity_description.streaming_value_fn(value)
|
||||
else:
|
||||
self._attr_native_value = None
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity):
|
||||
@ -676,7 +687,7 @@ class TeslemetryStreamTimeSensorEntity(TeslemetryVehicleStreamEntity, SensorEnti
|
||||
self.entity_description = description
|
||||
self._get_timestamp = ignore_variance(
|
||||
func=lambda value: dt_util.now()
|
||||
+ timedelta(minutes=description.streaming_value_fn(value)),
|
||||
+ timedelta(**{self.entity_description.streaming_unit: value}),
|
||||
ignored_variance=timedelta(minutes=description.variance),
|
||||
)
|
||||
super().__init__(data, description.key)
|
||||
@ -696,6 +707,7 @@ class TeslemetryStreamTimeSensorEntity(TeslemetryVehicleStreamEntity, SensorEnti
|
||||
self._attr_native_value = None
|
||||
else:
|
||||
self._attr_native_value = self._get_timestamp(value)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class TeslemetryVehicleTimeSensorEntity(TeslemetryVehicleEntity, SensorEntity):
|
||||
|
@ -72,7 +72,7 @@ class TimeEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Representation of a Time entity."""
|
||||
|
||||
entity_description: TimeEntityDescription
|
||||
_attr_native_value: time | None
|
||||
_attr_native_value: time | None = None
|
||||
_attr_device_class: None = None
|
||||
_attr_state: None = None
|
||||
|
||||
|
@ -31,6 +31,7 @@ from .const import (
|
||||
ATTR_MINUTES_DAY_SLEEP,
|
||||
ATTR_MINUTES_NIGHT_SLEEP,
|
||||
ATTR_MINUTES_REST,
|
||||
ATTR_POWER_SAVING,
|
||||
ATTR_SLEEP_LABEL,
|
||||
ATTR_TRACKER_STATE,
|
||||
CLIENT_ID,
|
||||
@ -277,6 +278,7 @@ class TractiveClient:
|
||||
payload = {
|
||||
ATTR_BATTERY_LEVEL: event["hardware"]["battery_level"],
|
||||
ATTR_TRACKER_STATE: event["tracker_state"].lower(),
|
||||
ATTR_POWER_SAVING: event.get("tracker_state_reason") == "POWER_SAVING",
|
||||
ATTR_BATTERY_CHARGING: event["charging_state"] == "CHARGING",
|
||||
}
|
||||
self._dispatch_tracker_event(
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@ -14,7 +16,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import Trackables, TractiveClient, TractiveConfigEntry
|
||||
from .const import TRACKER_HARDWARE_STATUS_UPDATED
|
||||
from .const import ATTR_POWER_SAVING, TRACKER_HARDWARE_STATUS_UPDATED
|
||||
from .entity import TractiveEntity
|
||||
|
||||
|
||||
@ -25,7 +27,7 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity):
|
||||
self,
|
||||
client: TractiveClient,
|
||||
item: Trackables,
|
||||
description: BinarySensorEntityDescription,
|
||||
description: TractiveBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize sensor entity."""
|
||||
super().__init__(
|
||||
@ -47,12 +49,27 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity):
|
||||
super().handle_status_update(event)
|
||||
|
||||
|
||||
SENSOR_TYPE = BinarySensorEntityDescription(
|
||||
key=ATTR_BATTERY_CHARGING,
|
||||
translation_key="tracker_battery_charging",
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TractiveBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Class describing Tractive binary sensor entities."""
|
||||
|
||||
supported: Callable[[dict], bool] = lambda _: True
|
||||
|
||||
|
||||
SENSOR_TYPES = [
|
||||
TractiveBinarySensorEntityDescription(
|
||||
key=ATTR_BATTERY_CHARGING,
|
||||
translation_key="tracker_battery_charging",
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
supported=lambda details: details.get("charging_state") is not None,
|
||||
),
|
||||
TractiveBinarySensorEntityDescription(
|
||||
key=ATTR_POWER_SAVING,
|
||||
translation_key="tracker_power_saving",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -65,9 +82,10 @@ async def async_setup_entry(
|
||||
trackables = entry.runtime_data.trackables
|
||||
|
||||
entities = [
|
||||
TractiveBinarySensor(client, item, SENSOR_TYPE)
|
||||
TractiveBinarySensor(client, item, description)
|
||||
for description in SENSOR_TYPES
|
||||
for item in trackables
|
||||
if item.tracker_details.get("charging_state") is not None
|
||||
if description.supported(item.tracker_details)
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
@ -16,6 +16,7 @@ ATTR_MINUTES_ACTIVE = "minutes_active"
|
||||
ATTR_MINUTES_DAY_SLEEP = "minutes_day_sleep"
|
||||
ATTR_MINUTES_NIGHT_SLEEP = "minutes_night_sleep"
|
||||
ATTR_MINUTES_REST = "minutes_rest"
|
||||
ATTR_POWER_SAVING = "power_saving"
|
||||
ATTR_SLEEP_LABEL = "sleep_label"
|
||||
ATTR_TRACKER_STATE = "tracker_state"
|
||||
|
||||
|
@ -22,6 +22,9 @@
|
||||
"binary_sensor": {
|
||||
"tracker_battery_charging": {
|
||||
"name": "Tracker battery charging"
|
||||
},
|
||||
"tracker_power_saving": {
|
||||
"name": "Tracker power saving"
|
||||
}
|
||||
},
|
||||
"device_tracker": {
|
||||
|
@ -14,8 +14,6 @@ import mimetypes
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import subprocess
|
||||
import tempfile
|
||||
from time import monotonic
|
||||
from typing import Any, Final
|
||||
|
||||
@ -309,80 +307,73 @@ async def _async_convert_audio(
|
||||
) -> AsyncGenerator[bytes]:
|
||||
"""Convert audio to a preferred format using ffmpeg."""
|
||||
ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass)
|
||||
audio_bytes = b"".join([chunk async for chunk in audio_bytes_gen])
|
||||
data = await hass.async_add_executor_job(
|
||||
lambda: _convert_audio(
|
||||
ffmpeg_manager.binary,
|
||||
from_extension,
|
||||
audio_bytes,
|
||||
to_extension,
|
||||
to_sample_rate=to_sample_rate,
|
||||
to_sample_channels=to_sample_channels,
|
||||
to_sample_bytes=to_sample_bytes,
|
||||
)
|
||||
|
||||
command = [
|
||||
ffmpeg_manager.binary,
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-f",
|
||||
from_extension,
|
||||
"-i",
|
||||
"pipe:",
|
||||
"-f",
|
||||
to_extension,
|
||||
]
|
||||
if to_sample_rate is not None:
|
||||
command.extend(["-ar", str(to_sample_rate)])
|
||||
if to_sample_channels is not None:
|
||||
command.extend(["-ac", str(to_sample_channels)])
|
||||
if to_extension == "mp3":
|
||||
# Max quality for MP3.
|
||||
command.extend(["-q:a", "0"])
|
||||
if to_sample_bytes == 2:
|
||||
# 16-bit samples.
|
||||
command.extend(["-sample_fmt", "s16"])
|
||||
command.append("pipe:1") # Send output to stdout.
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*command,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
yield data
|
||||
|
||||
async def write_input() -> None:
|
||||
assert process.stdin
|
||||
try:
|
||||
async for chunk in audio_bytes_gen:
|
||||
process.stdin.write(chunk)
|
||||
await process.stdin.drain()
|
||||
finally:
|
||||
if process.stdin:
|
||||
process.stdin.close()
|
||||
|
||||
def _convert_audio(
|
||||
ffmpeg_binary: str,
|
||||
from_extension: str,
|
||||
audio_bytes: bytes,
|
||||
to_extension: str,
|
||||
to_sample_rate: int | None = None,
|
||||
to_sample_channels: int | None = None,
|
||||
to_sample_bytes: int | None = None,
|
||||
) -> bytes:
|
||||
"""Convert audio to a preferred format using ffmpeg."""
|
||||
writer_task = hass.async_create_background_task(
|
||||
write_input(), "tts_ffmpeg_conversion"
|
||||
)
|
||||
|
||||
# We have to use a temporary file here because some formats like WAV store
|
||||
# the length of the file in the header, and therefore cannot be written in a
|
||||
# streaming fashion.
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="wb+", suffix=f".{to_extension}"
|
||||
) as output_file:
|
||||
# input
|
||||
command = [
|
||||
ffmpeg_binary,
|
||||
"-y", # overwrite temp file
|
||||
"-f",
|
||||
from_extension,
|
||||
"-i",
|
||||
"pipe:", # input from stdin
|
||||
]
|
||||
|
||||
# output
|
||||
command.extend(["-f", to_extension])
|
||||
|
||||
if to_sample_rate is not None:
|
||||
command.extend(["-ar", str(to_sample_rate)])
|
||||
|
||||
if to_sample_channels is not None:
|
||||
command.extend(["-ac", str(to_sample_channels)])
|
||||
|
||||
if to_extension == "mp3":
|
||||
# Max quality for MP3
|
||||
command.extend(["-q:a", "0"])
|
||||
|
||||
if to_sample_bytes == 2:
|
||||
# 16-bit samples
|
||||
command.extend(["-sample_fmt", "s16"])
|
||||
|
||||
command.append(output_file.name)
|
||||
|
||||
with subprocess.Popen(
|
||||
command, stdin=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
) as proc:
|
||||
_stdout, stderr = proc.communicate(input=audio_bytes)
|
||||
if proc.returncode != 0:
|
||||
_LOGGER.error(stderr.decode())
|
||||
raise RuntimeError(
|
||||
f"Unexpected error while running ffmpeg with arguments: {command}."
|
||||
"See log for details."
|
||||
)
|
||||
|
||||
output_file.seek(0)
|
||||
return output_file.read()
|
||||
assert process.stdout
|
||||
chunk_size = 4096
|
||||
try:
|
||||
while True:
|
||||
chunk = await process.stdout.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
finally:
|
||||
# Ensure we wait for the input writer to complete.
|
||||
await writer_task
|
||||
# Wait for process termination and check for errors.
|
||||
retcode = await process.wait()
|
||||
if retcode != 0:
|
||||
assert process.stderr
|
||||
stderr_data = await process.stderr.read()
|
||||
_LOGGER.error(stderr_data.decode())
|
||||
raise RuntimeError(
|
||||
f"Unexpected error while running ffmpeg with arguments: {command}. "
|
||||
"See log for details."
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
@ -178,7 +178,11 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
self._pipeline_ended_event.set()
|
||||
self.device.set_is_active(False)
|
||||
elif event.type == assist_pipeline.PipelineEventType.WAKE_WORD_START:
|
||||
self.hass.add_job(self._client.write_event(Detect().event()))
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._client.write_event(Detect().event()),
|
||||
f"{self.entity_id} {event.type}",
|
||||
)
|
||||
elif event.type == assist_pipeline.PipelineEventType.WAKE_WORD_END:
|
||||
# Wake word detection
|
||||
# Inform client of wake word detection
|
||||
@ -187,46 +191,59 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
name=wake_word_output["wake_word_id"],
|
||||
timestamp=wake_word_output.get("timestamp"),
|
||||
)
|
||||
self.hass.add_job(self._client.write_event(detection.event()))
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._client.write_event(detection.event()),
|
||||
f"{self.entity_id} {event.type}",
|
||||
)
|
||||
elif event.type == assist_pipeline.PipelineEventType.STT_START:
|
||||
# Speech-to-text
|
||||
self.device.set_is_active(True)
|
||||
|
||||
if event.data:
|
||||
self.hass.add_job(
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._client.write_event(
|
||||
Transcribe(language=event.data["metadata"]["language"]).event()
|
||||
)
|
||||
),
|
||||
f"{self.entity_id} {event.type}",
|
||||
)
|
||||
elif event.type == assist_pipeline.PipelineEventType.STT_VAD_START:
|
||||
# User started speaking
|
||||
if event.data:
|
||||
self.hass.add_job(
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._client.write_event(
|
||||
VoiceStarted(timestamp=event.data["timestamp"]).event()
|
||||
)
|
||||
),
|
||||
f"{self.entity_id} {event.type}",
|
||||
)
|
||||
elif event.type == assist_pipeline.PipelineEventType.STT_VAD_END:
|
||||
# User stopped speaking
|
||||
if event.data:
|
||||
self.hass.add_job(
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._client.write_event(
|
||||
VoiceStopped(timestamp=event.data["timestamp"]).event()
|
||||
)
|
||||
),
|
||||
f"{self.entity_id} {event.type}",
|
||||
)
|
||||
elif event.type == assist_pipeline.PipelineEventType.STT_END:
|
||||
# Speech-to-text transcript
|
||||
if event.data:
|
||||
# Inform client of transript
|
||||
stt_text = event.data["stt_output"]["text"]
|
||||
self.hass.add_job(
|
||||
self._client.write_event(Transcript(text=stt_text).event())
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._client.write_event(Transcript(text=stt_text).event()),
|
||||
f"{self.entity_id} {event.type}",
|
||||
)
|
||||
elif event.type == assist_pipeline.PipelineEventType.TTS_START:
|
||||
# Text-to-speech text
|
||||
if event.data:
|
||||
# Inform client of text
|
||||
self.hass.add_job(
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._client.write_event(
|
||||
Synthesize(
|
||||
text=event.data["tts_input"],
|
||||
@ -235,22 +252,32 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
language=event.data.get("language"),
|
||||
),
|
||||
).event()
|
||||
)
|
||||
),
|
||||
f"{self.entity_id} {event.type}",
|
||||
)
|
||||
elif event.type == assist_pipeline.PipelineEventType.TTS_END:
|
||||
# TTS stream
|
||||
if event.data and (tts_output := event.data["tts_output"]):
|
||||
media_id = tts_output["media_id"]
|
||||
self.hass.add_job(self._stream_tts(media_id))
|
||||
if (
|
||||
event.data
|
||||
and (tts_output := event.data["tts_output"])
|
||||
and (stream := tts.async_get_stream(self.hass, tts_output["token"]))
|
||||
):
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._stream_tts(stream),
|
||||
f"{self.entity_id} {event.type}",
|
||||
)
|
||||
elif event.type == assist_pipeline.PipelineEventType.ERROR:
|
||||
# Pipeline error
|
||||
if event.data:
|
||||
self.hass.add_job(
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._client.write_event(
|
||||
Error(
|
||||
text=event.data["message"], code=event.data["code"]
|
||||
).event()
|
||||
)
|
||||
),
|
||||
f"{self.entity_id} {event.type}",
|
||||
)
|
||||
|
||||
async def async_announce(self, announcement: AssistSatelliteAnnouncement) -> None:
|
||||
@ -662,13 +689,16 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
await self._client.disconnect()
|
||||
self._client = None
|
||||
|
||||
async def _stream_tts(self, media_id: str) -> None:
|
||||
async def _stream_tts(self, tts_result: tts.ResultStream) -> None:
|
||||
"""Stream TTS WAV audio to satellite in chunks."""
|
||||
assert self._client is not None
|
||||
|
||||
extension, data = await tts.async_get_media_source_audio(self.hass, media_id)
|
||||
if extension != "wav":
|
||||
raise ValueError(f"Cannot stream audio format to satellite: {extension}")
|
||||
if tts_result.extension != "wav":
|
||||
raise ValueError(
|
||||
f"Cannot stream audio format to satellite: {tts_result.extension}"
|
||||
)
|
||||
|
||||
data = b"".join([chunk async for chunk in tts_result.async_stream_result()])
|
||||
|
||||
with io.BytesIO(data) as wav_io, wave.open(wav_io, "rb") as wav_file:
|
||||
sample_rate = wav_file.getframerate()
|
||||
|
@ -21,7 +21,7 @@
|
||||
"zha",
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": ["zha==0.0.55"],
|
||||
"requirements": ["zha==0.0.56"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10C4",
|
||||
|
@ -629,6 +629,10 @@ class _ScriptRun:
|
||||
self, script: Script, *, parallel: bool = False
|
||||
) -> None:
|
||||
"""Execute a script."""
|
||||
if not script.enabled:
|
||||
self._log("Skipping disabled script: %s", script.name)
|
||||
trace_set_result(enabled=False)
|
||||
return
|
||||
result = await self._async_run_long_action(
|
||||
self._hass.async_create_task_internal(
|
||||
script.async_run(
|
||||
@ -1442,8 +1446,12 @@ class Script:
|
||||
script_mode: str = DEFAULT_SCRIPT_MODE,
|
||||
top_level: bool = True,
|
||||
variables: ScriptVariables | None = None,
|
||||
enabled: bool = True,
|
||||
) -> None:
|
||||
"""Initialize the script."""
|
||||
"""Initialize the script.
|
||||
|
||||
enabled attribute is only used for non-top-level scripts.
|
||||
"""
|
||||
if not (all_scripts := hass.data.get(DATA_SCRIPTS)):
|
||||
all_scripts = hass.data[DATA_SCRIPTS] = []
|
||||
hass.bus.async_listen_once(
|
||||
@ -1462,6 +1470,7 @@ class Script:
|
||||
self.name = name
|
||||
self.unique_id = f"{domain}.{name}-{id(self)}"
|
||||
self.domain = domain
|
||||
self.enabled = enabled
|
||||
self.running_description = running_description or f"{domain} script"
|
||||
self._change_listener = change_listener
|
||||
self._change_listener_job = (
|
||||
@ -2002,6 +2011,7 @@ class Script:
|
||||
max_runs=self.max_runs,
|
||||
logger=self._logger,
|
||||
top_level=False,
|
||||
enabled=parallel_script.get(CONF_ENABLED, True),
|
||||
)
|
||||
parallel_script.change_listener = partial(
|
||||
self._chain_change_listener, parallel_script
|
||||
|
@ -127,6 +127,7 @@
|
||||
"discharging": "Discharging",
|
||||
"disconnected": "Disconnected",
|
||||
"enabled": "Enabled",
|
||||
"error": "Error",
|
||||
"high": "High",
|
||||
"home": "Home",
|
||||
"idle": "Idle",
|
||||
|
6
requirements_all.txt
generated
6
requirements_all.txt
generated
@ -1852,7 +1852,7 @@ pybbox==0.0.5-alpha
|
||||
pyblackbird==0.6
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
pyblu==2.0.0
|
||||
pyblu==2.0.1
|
||||
|
||||
# homeassistant.components.neato
|
||||
pybotvac==0.0.26
|
||||
@ -2331,7 +2331,7 @@ pysmartthings==3.0.4
|
||||
pysmarty2==0.10.2
|
||||
|
||||
# homeassistant.components.smhi
|
||||
pysmhi==1.0.1
|
||||
pysmhi==1.0.2
|
||||
|
||||
# homeassistant.components.edl21
|
||||
pysml==0.0.12
|
||||
@ -3158,7 +3158,7 @@ zeroconf==0.146.5
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.55
|
||||
zha==0.0.56
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.13
|
||||
|
@ -35,19 +35,19 @@ requests-mock==1.12.1
|
||||
respx==0.22.0
|
||||
syrupy==4.8.1
|
||||
tqdm==4.67.1
|
||||
types-aiofiles==24.1.0.20241221
|
||||
types-aiofiles==24.1.0.20250326
|
||||
types-atomicwrites==1.4.5.1
|
||||
types-croniter==5.0.1.20241205
|
||||
types-croniter==6.0.0.20250411
|
||||
types-caldav==1.3.0.20241107
|
||||
types-chardet==0.1.5
|
||||
types-decorator==5.1.8.20250121
|
||||
types-decorator==5.2.0.20250324
|
||||
types-pexpect==4.9.0.20241208
|
||||
types-protobuf==5.29.1.20241207
|
||||
types-psutil==6.1.0.20241221
|
||||
types-pyserial==3.5.0.20250130
|
||||
types-protobuf==5.29.1.20250403
|
||||
types-psutil==7.0.0.20250401
|
||||
types-pyserial==3.5.0.20250326
|
||||
types-python-dateutil==2.9.0.20241206
|
||||
types-python-slugify==8.0.2.20240310
|
||||
types-pytz==2025.1.0.20250204
|
||||
types-PyYAML==6.0.12.20241230
|
||||
types-pytz==2025.2.0.20250326
|
||||
types-PyYAML==6.0.12.20250402
|
||||
types-requests==2.31.0.3
|
||||
types-xmltodict==0.13.0.3
|
||||
|
6
requirements_test_all.txt
generated
6
requirements_test_all.txt
generated
@ -1530,7 +1530,7 @@ pybalboa==1.1.3
|
||||
pyblackbird==0.6
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
pyblu==2.0.0
|
||||
pyblu==2.0.1
|
||||
|
||||
# homeassistant.components.neato
|
||||
pybotvac==0.0.26
|
||||
@ -1904,7 +1904,7 @@ pysmartthings==3.0.4
|
||||
pysmarty2==0.10.2
|
||||
|
||||
# homeassistant.components.smhi
|
||||
pysmhi==1.0.1
|
||||
pysmhi==1.0.2
|
||||
|
||||
# homeassistant.components.edl21
|
||||
pysml==0.0.12
|
||||
@ -2554,7 +2554,7 @@ zeroconf==0.146.5
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.55
|
||||
zha==0.0.56
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.62.0
|
||||
|
@ -196,13 +196,13 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: "none",
|
||||
CONF_PROMPT: "bla",
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_TEMPERATURE: 0.3,
|
||||
CONF_LLM_HASS_API: [],
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
@ -224,15 +224,32 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_PROMPT: "",
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_PROMPT: "",
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_PROMPT: "",
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_PROMPT: "",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_PROMPT: "",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_options_switching(
|
||||
|
@ -23,8 +23,7 @@ from . import (
|
||||
@pytest.fixture(name="disable_bluez_manager_socket", autouse=True, scope="package")
|
||||
def disable_bluez_manager_socket():
|
||||
"""Mock the bluez manager socket."""
|
||||
with patch.object(bleak_manager, "get_global_bluez_manager_with_timeout"):
|
||||
yield
|
||||
bleak_manager.get_global_bluez_manager_with_timeout._has_dbus_socket = False
|
||||
|
||||
|
||||
@pytest.fixture(name="disable_dbus_socket", autouse=True, scope="package")
|
||||
|
@ -178,6 +178,22 @@ async def test_reproducing_states(
|
||||
| CoverEntityFeature.OPEN,
|
||||
},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"cover.closed_supports_all_features",
|
||||
CoverState.CLOSED,
|
||||
{
|
||||
ATTR_CURRENT_POSITION: 0,
|
||||
ATTR_CURRENT_TILT_POSITION: 0,
|
||||
ATTR_SUPPORTED_FEATURES: CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
| CoverEntityFeature.STOP
|
||||
| CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.STOP_TILT
|
||||
| CoverEntityFeature.SET_TILT_POSITION,
|
||||
},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"cover.tilt_only_open",
|
||||
CoverState.OPEN,
|
||||
@ -249,6 +265,14 @@ async def test_reproducing_states(
|
||||
await async_reproduce_state(
|
||||
hass,
|
||||
[
|
||||
State(
|
||||
"cover.closed_supports_all_features",
|
||||
CoverState.CLOSED,
|
||||
{
|
||||
ATTR_CURRENT_POSITION: 0,
|
||||
ATTR_CURRENT_TILT_POSITION: 0,
|
||||
},
|
||||
),
|
||||
State("cover.entity_close", CoverState.CLOSED),
|
||||
State("cover.closed_only_supports_close_open", CoverState.CLOSED),
|
||||
State("cover.closed_only_supports_tilt_close_open", CoverState.CLOSED),
|
||||
@ -364,6 +388,11 @@ async def test_reproducing_states(
|
||||
await async_reproduce_state(
|
||||
hass,
|
||||
[
|
||||
State(
|
||||
"cover.closed_supports_all_features",
|
||||
CoverState.CLOSED,
|
||||
{ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 50},
|
||||
),
|
||||
State("cover.entity_close", CoverState.OPEN),
|
||||
State(
|
||||
"cover.closed_only_supports_close_open",
|
||||
@ -458,7 +487,6 @@ async def test_reproducing_states(
|
||||
valid_close_calls = [
|
||||
{"entity_id": "cover.entity_open"},
|
||||
{"entity_id": "cover.entity_open_attr"},
|
||||
{"entity_id": "cover.entity_entirely_open"},
|
||||
{"entity_id": "cover.open_only_supports_close_open"},
|
||||
{"entity_id": "cover.open_missing_all_features"},
|
||||
]
|
||||
@ -481,11 +509,8 @@ async def test_reproducing_states(
|
||||
valid_open_calls.remove(call.data)
|
||||
|
||||
valid_close_tilt_calls = [
|
||||
{"entity_id": "cover.entity_open_tilt"},
|
||||
{"entity_id": "cover.entity_entirely_open"},
|
||||
{"entity_id": "cover.tilt_only_open"},
|
||||
{"entity_id": "cover.entity_open_attr"},
|
||||
{"entity_id": "cover.tilt_only_tilt_position_100"},
|
||||
{"entity_id": "cover.open_only_supports_tilt_close_open"},
|
||||
]
|
||||
assert len(close_tilt_calls) == len(valid_close_tilt_calls)
|
||||
@ -495,9 +520,7 @@ async def test_reproducing_states(
|
||||
valid_close_tilt_calls.remove(call.data)
|
||||
|
||||
valid_open_tilt_calls = [
|
||||
{"entity_id": "cover.entity_close_tilt"},
|
||||
{"entity_id": "cover.tilt_only_closed"},
|
||||
{"entity_id": "cover.tilt_only_tilt_position_0"},
|
||||
{"entity_id": "cover.closed_only_supports_tilt_close_open"},
|
||||
]
|
||||
assert len(open_tilt_calls) == len(valid_open_tilt_calls)
|
||||
@ -523,6 +546,14 @@ async def test_reproducing_states(
|
||||
"entity_id": "cover.open_only_supports_position",
|
||||
ATTR_POSITION: 0,
|
||||
},
|
||||
{
|
||||
"entity_id": "cover.closed_supports_all_features",
|
||||
ATTR_POSITION: 0,
|
||||
},
|
||||
{
|
||||
"entity_id": "cover.entity_entirely_open",
|
||||
ATTR_POSITION: 0,
|
||||
},
|
||||
]
|
||||
assert len(position_calls) == len(valid_position_calls)
|
||||
for call in position_calls:
|
||||
@ -551,7 +582,34 @@ async def test_reproducing_states(
|
||||
"entity_id": "cover.tilt_partial_open_only_supports_tilt_position",
|
||||
ATTR_TILT_POSITION: 70,
|
||||
},
|
||||
{
|
||||
"entity_id": "cover.closed_supports_all_features",
|
||||
ATTR_TILT_POSITION: 50,
|
||||
},
|
||||
{
|
||||
"entity_id": "cover.entity_close_tilt",
|
||||
ATTR_TILT_POSITION: 100,
|
||||
},
|
||||
{
|
||||
"entity_id": "cover.entity_open_tilt",
|
||||
ATTR_TILT_POSITION: 0,
|
||||
},
|
||||
{
|
||||
"entity_id": "cover.entity_entirely_open",
|
||||
ATTR_TILT_POSITION: 0,
|
||||
},
|
||||
{
|
||||
"entity_id": "cover.tilt_only_tilt_position_100",
|
||||
ATTR_TILT_POSITION: 0,
|
||||
},
|
||||
{
|
||||
"entity_id": "cover.tilt_only_tilt_position_0",
|
||||
ATTR_TILT_POSITION: 100,
|
||||
},
|
||||
]
|
||||
for call in position_tilt_calls:
|
||||
if ATTR_TILT_POSITION not in call.data:
|
||||
continue
|
||||
assert len(position_tilt_calls) == len(valid_position_tilt_calls)
|
||||
for call in position_tilt_calls:
|
||||
assert call.domain == "cover"
|
||||
|
@ -813,12 +813,15 @@ async def test_reauth_confirm_valid(
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053, CONF_PASSWORD: ""},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||
)
|
||||
@ -828,6 +831,48 @@ async def test_reauth_confirm_valid(
|
||||
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||
async def test_reauth_attempt_to_change_mac_aborts(
|
||||
hass: HomeAssistant, mock_client: APIClient
|
||||
) -> None:
|
||||
"""Test reauth initiation with valid PSK attempting to change mac.
|
||||
|
||||
This can happen if reauth starts, but they don't finish it before
|
||||
a new device takes the place of the old one at the same IP.
|
||||
"""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:bb"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_unique_id_changed"
|
||||
assert CONF_NOISE_PSK not in entry.data
|
||||
assert result["description_placeholders"] == {
|
||||
"expected_mac": "11:22:33:44:55:aa",
|
||||
"host": "127.0.0.1",
|
||||
"name": "test",
|
||||
"unexpected_device_name": "test",
|
||||
"unexpected_mac": "11:22:33:44:55:bb",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_reauth_fixed_via_dashboard(
|
||||
hass: HomeAssistant,
|
||||
@ -845,10 +890,13 @@ async def test_reauth_fixed_via_dashboard(
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
|
||||
mock_dashboard["configured"].append(
|
||||
{
|
||||
@ -883,7 +931,7 @@ async def test_reauth_fixed_via_dashboard_add_encryption_remove_password(
|
||||
"""Test reauth fixed automatically via dashboard with password removed."""
|
||||
mock_client.device_info.side_effect = (
|
||||
InvalidAuthAPIError,
|
||||
DeviceInfo(uses_password=False, name="test"),
|
||||
DeviceInfo(uses_password=False, name="test", mac_address="11:22:33:44:55:aa"),
|
||||
)
|
||||
|
||||
mock_dashboard["configured"].append(
|
||||
@ -917,7 +965,9 @@ async def test_reauth_fixed_via_remove_password(
|
||||
mock_setup_entry: None,
|
||||
) -> None:
|
||||
"""Test reauth fixed automatically by seeing password removed."""
|
||||
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
@ -943,10 +993,13 @@ async def test_reauth_fixed_via_dashboard_at_confirm(
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
|
||||
@ -984,6 +1037,7 @@ async def test_reauth_confirm_invalid(
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053, CONF_PASSWORD: ""},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
@ -1000,7 +1054,9 @@ async def test_reauth_confirm_invalid(
|
||||
assert result["errors"]["base"] == "invalid_psk"
|
||||
|
||||
mock_client.device_info = AsyncMock(
|
||||
return_value=DeviceInfo(uses_password=False, name="test")
|
||||
return_value=DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||
@ -1019,7 +1075,7 @@ async def test_reauth_confirm_invalid_with_unique_id(
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053, CONF_PASSWORD: ""},
|
||||
unique_id="test",
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
@ -1036,7 +1092,9 @@ async def test_reauth_confirm_invalid_with_unique_id(
|
||||
assert result["errors"]["base"] == "invalid_psk"
|
||||
|
||||
mock_client.device_info = AsyncMock(
|
||||
return_value=DeviceInfo(uses_password=False, name="test")
|
||||
return_value=DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK}
|
||||
@ -1049,7 +1107,7 @@ async def test_reauth_confirm_invalid_with_unique_id(
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_reauth_encryption_key_removed(
|
||||
hass: HomeAssistant, mock_client, mock_setup_entry: None
|
||||
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
|
||||
) -> None:
|
||||
"""Test reauth when the encryption key was removed."""
|
||||
entry = MockConfigEntry(
|
||||
@ -1060,7 +1118,7 @@ async def test_reauth_encryption_key_removed(
|
||||
CONF_PASSWORD: "",
|
||||
CONF_NOISE_PSK: VALID_NOISE_PSK,
|
||||
},
|
||||
unique_id="test",
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
@ -1660,7 +1718,11 @@ async def test_user_flow_name_conflict_migrate(
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "name_conflict_migrated"
|
||||
|
||||
assert result["description_placeholders"] == {
|
||||
"existing_mac": "11:22:33:44:55:cc",
|
||||
"mac": "11:22:33:44:55:aa",
|
||||
"name": "test",
|
||||
}
|
||||
assert existing_entry.data == {
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
@ -1715,3 +1777,321 @@ async def test_user_flow_name_conflict_overwrite(
|
||||
CONF_DEVICE_NAME: "test",
|
||||
}
|
||||
assert result["context"]["unique_id"] == "11:22:33:44:55:aa"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||
async def test_reconfig_success_with_same_ip_new_name(
|
||||
hass: HomeAssistant, mock_client: APIClient
|
||||
) -> None:
|
||||
"""Test reconfig initiation with same ip and new name."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="other", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert entry.data[CONF_HOST] == "127.0.0.1"
|
||||
assert entry.data[CONF_DEVICE_NAME] == "other"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||
async def test_reconfig_success_with_new_ip_new_name(
|
||||
hass: HomeAssistant, mock_client: APIClient
|
||||
) -> None:
|
||||
"""Test reconfig initiation with new ip and new name."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="other", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "127.0.0.2", CONF_PORT: 6053}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert entry.data[CONF_HOST] == "127.0.0.2"
|
||||
assert entry.data[CONF_DEVICE_NAME] == "other"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||
async def test_reconfig_success_with_new_ip_same_name(
|
||||
hass: HomeAssistant, mock_client: APIClient
|
||||
) -> None:
|
||||
"""Test reconfig initiation with new ip and same name."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
CONF_NOISE_PSK: VALID_NOISE_PSK,
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert entry.data[CONF_HOST] == "127.0.0.1"
|
||||
assert entry.data[CONF_DEVICE_NAME] == "test"
|
||||
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||
async def test_reconfig_name_conflict_with_existing_entry(
|
||||
hass: HomeAssistant, mock_client: APIClient
|
||||
) -> None:
|
||||
"""Test reconfig with a name conflict with an existing entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
entry2 = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "other",
|
||||
},
|
||||
unique_id="11:22:33:44:55:bb",
|
||||
)
|
||||
entry2.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="other", mac_address="11:22:33:44:55:aa"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "127.0.0.3", CONF_PORT: 6053}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_name_conflict"
|
||||
assert result["description_placeholders"] == {
|
||||
"existing_title": "Mock Title",
|
||||
"expected_mac": "11:22:33:44:55:aa",
|
||||
"host": "127.0.0.3",
|
||||
"name": "test",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||
async def test_reconfig_attempt_to_change_mac_aborts(
|
||||
hass: HomeAssistant, mock_client: APIClient
|
||||
) -> None:
|
||||
"""Test reconfig initiation with valid PSK attempting to change mac."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="other", mac_address="11:22:33:44:55:bb"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "127.0.0.2", CONF_PORT: 6053}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_unique_id_changed"
|
||||
assert CONF_NOISE_PSK not in entry.data
|
||||
assert result["description_placeholders"] == {
|
||||
"expected_mac": "11:22:33:44:55:aa",
|
||||
"host": "127.0.0.2",
|
||||
"name": "test",
|
||||
"unexpected_device_name": "other",
|
||||
"unexpected_mac": "11:22:33:44:55:bb",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||
async def test_reconfig_mac_used_by_other_entry(
|
||||
hass: HomeAssistant, mock_client: APIClient
|
||||
) -> None:
|
||||
"""Test reconfig when there is another entry for the mac."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
entry2 = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test4",
|
||||
},
|
||||
unique_id="11:22:33:44:55:bb",
|
||||
)
|
||||
entry2.add_to_hass(hass)
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:bb"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "127.0.0.2", CONF_PORT: 6053}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||
async def test_reconfig_name_conflict_migrate(
|
||||
hass: HomeAssistant, mock_client: APIClient
|
||||
) -> None:
|
||||
"""Test reconfig initiation when device has been replaced."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:bb"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "127.0.0.2", CONF_PORT: 6053}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "name_conflict"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "name_conflict_migrate"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "name_conflict_migrated"
|
||||
|
||||
assert entry.data == {
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_NOISE_PSK: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
}
|
||||
assert entry.unique_id == "11:22:33:44:55:bb"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")
|
||||
async def test_reconfig_name_conflict_overwrite(
|
||||
hass: HomeAssistant, mock_client: APIClient
|
||||
) -> None:
|
||||
"""Test reconfig initiation when device has been replaced."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_client.device_info.return_value = DeviceInfo(
|
||||
uses_password=False, name="test", mac_address="11:22:33:44:55:bb"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "127.0.0.2", CONF_PORT: 6053}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "name_conflict"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "name_conflict_overwrite"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_NOISE_PSK: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
}
|
||||
assert result["context"]["unique_id"] == "11:22:33:44:55:bb"
|
||||
assert (
|
||||
hass.config_entries.async_entry_for_domain_unique_id(
|
||||
DOMAIN, "11:22:33:44:55:aa"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
@ -193,7 +193,7 @@ async def test_new_dashboard_fix_reauth(
|
||||
"""Test config entries waiting for reauth are triggered."""
|
||||
mock_client.device_info.side_effect = (
|
||||
InvalidAuthAPIError,
|
||||
DeviceInfo(uses_password=False, name="test"),
|
||||
DeviceInfo(uses_password=False, name="test", mac_address="11:22:33:44:55:AA"),
|
||||
)
|
||||
|
||||
with patch(
|
||||
|
@ -500,6 +500,6 @@ async def test_esphome_device_without_friendly_name(
|
||||
states=states,
|
||||
device_info={"friendly_name": None},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.my_binary_sensor")
|
||||
state = hass.states.get("binary_sensor.test_mybinary_sensor")
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
@ -1577,3 +1577,51 @@ async def test_entry_missing_bluetooth_mac_address(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.data[CONF_BLUETOOTH_MAC_ADDRESS] == "AA:BB:CC:DD:EE:FC"
|
||||
|
||||
|
||||
async def test_device_adds_friendly_name(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: Callable[
|
||||
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
|
||||
Awaitable[MockESPHomeDevice],
|
||||
],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a device with user services that change arguments."""
|
||||
entity_info = []
|
||||
states = []
|
||||
device: MockESPHomeDevice = await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=[],
|
||||
device_info={"name": "nofriendlyname", "friendly_name": ""},
|
||||
states=states,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
dev_reg = dr.async_get(hass)
|
||||
dev = dev_reg.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device.entry.unique_id)}
|
||||
)
|
||||
assert dev.name == "Nofriendlyname"
|
||||
assert (
|
||||
"No `friendly_name` set in the `esphome:` section of "
|
||||
"the YAML config for device 'nofriendlyname'"
|
||||
) in caplog.text
|
||||
caplog.clear()
|
||||
|
||||
await device.mock_disconnect(True)
|
||||
await hass.async_block_till_done()
|
||||
device.device_info = DeviceInfo(
|
||||
**{**device.device_info.to_dict(), "friendly_name": "I have a friendly name"}
|
||||
)
|
||||
mock_client.device_info = AsyncMock(return_value=device.device_info)
|
||||
await device.mock_connect()
|
||||
await hass.async_block_till_done()
|
||||
dev = dev_reg.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device.entry.unique_id)}
|
||||
)
|
||||
assert dev.name == "I have a friendly name"
|
||||
assert (
|
||||
"No `friendly_name` set in the `esphome:` section of the YAML config for device"
|
||||
) not in caplog.text
|
||||
|
@ -125,7 +125,6 @@ def will_options_be_rendered_again(current_options, new_options) -> bool:
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: "none",
|
||||
CONF_PROMPT: "bla",
|
||||
},
|
||||
{
|
||||
@ -162,12 +161,12 @@ def will_options_be_rendered_again(current_options, new_options) -> bool:
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_PROMPT: "",
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_PROMPT: "",
|
||||
},
|
||||
None,
|
||||
@ -235,7 +234,7 @@ def will_options_be_rendered_again(current_options, new_options) -> bool:
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_TEMPERATURE: 0.3,
|
||||
CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
@ -263,6 +262,24 @@ def will_options_be_rendered_again(current_options, new_options) -> bool:
|
||||
},
|
||||
{CONF_USE_GOOGLE_SEARCH_TOOL: "invalid_google_search_option"},
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_PROMPT: "",
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_PROMPT: "",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_PROMPT: "",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
},
|
||||
None,
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
|
@ -17,7 +17,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@ -84,7 +84,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@ -151,7 +151,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@ -218,7 +218,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
|
@ -1,57 +1 @@
|
||||
"""Tests for the jewish_calendar component."""
|
||||
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.components import jewish_calendar
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LatLng = namedtuple("_LatLng", ["lat", "lng"]) # noqa: PYI024
|
||||
|
||||
HDATE_DEFAULT_ALTITUDE = 754
|
||||
NYC_LATLNG = _LatLng(40.7128, -74.0060)
|
||||
JERUSALEM_LATLNG = _LatLng(31.778, 35.235)
|
||||
|
||||
|
||||
def make_nyc_test_params(dtime, results, havdalah_offset=0):
|
||||
"""Make test params for NYC."""
|
||||
if isinstance(results, dict):
|
||||
time_zone = dt_util.get_time_zone("America/New_York")
|
||||
results = {
|
||||
key: value.replace(tzinfo=time_zone)
|
||||
if isinstance(value, datetime)
|
||||
else value
|
||||
for key, value in results.items()
|
||||
}
|
||||
return (
|
||||
dtime,
|
||||
jewish_calendar.DEFAULT_CANDLE_LIGHT,
|
||||
havdalah_offset,
|
||||
True,
|
||||
"America/New_York",
|
||||
NYC_LATLNG.lat,
|
||||
NYC_LATLNG.lng,
|
||||
results,
|
||||
)
|
||||
|
||||
|
||||
def make_jerusalem_test_params(dtime, results, havdalah_offset=0):
|
||||
"""Make test params for Jerusalem."""
|
||||
if isinstance(results, dict):
|
||||
time_zone = dt_util.get_time_zone("Asia/Jerusalem")
|
||||
results = {
|
||||
key: value.replace(tzinfo=time_zone)
|
||||
if isinstance(value, datetime)
|
||||
else value
|
||||
for key, value in results.items()
|
||||
}
|
||||
return (
|
||||
dtime,
|
||||
40,
|
||||
havdalah_offset,
|
||||
False,
|
||||
"Asia/Jerusalem",
|
||||
JERUSALEM_LATLNG.lat,
|
||||
JERUSALEM_LATLNG.lng,
|
||||
results,
|
||||
)
|
||||
|
@ -1,22 +1,39 @@
|
||||
"""Common fixtures for the jewish_calendar tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import AsyncGenerator, Generator, Iterable
|
||||
import datetime as dt
|
||||
from typing import NamedTuple
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.jewish_calendar.const import DEFAULT_NAME, DOMAIN
|
||||
from homeassistant.components.jewish_calendar.const import (
|
||||
CONF_CANDLE_LIGHT_MINUTES,
|
||||
CONF_DIASPORA,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_LANGUAGE, CONF_TIME_ZONE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title=DEFAULT_NAME,
|
||||
domain=DOMAIN,
|
||||
)
|
||||
class _LocationData(NamedTuple):
|
||||
timezone: str
|
||||
diaspora: bool
|
||||
lat: float
|
||||
lng: float
|
||||
candle_lighting: int
|
||||
|
||||
|
||||
LOCATIONS = {
|
||||
"Jerusalem": _LocationData("Asia/Jerusalem", False, 31.7683, 35.2137, 40),
|
||||
"New York": _LocationData("America/New_York", True, 40.7128, -74.006, 18),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -26,3 +43,109 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"homeassistant.components.jewish_calendar.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def location_data(request: pytest.FixtureRequest) -> _LocationData | None:
|
||||
"""Return data based on location name."""
|
||||
if not hasattr(request, "param"):
|
||||
return None
|
||||
|
||||
return LOCATIONS[request.param]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tz_info(hass: HomeAssistant, location_data: _LocationData | None) -> dt.tzinfo:
|
||||
"""Return time zone info."""
|
||||
if location_data is None:
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
return dt_util.get_time_zone(location_data.timezone)
|
||||
|
||||
|
||||
@pytest.fixture(name="test_time")
|
||||
def _test_time(
|
||||
request: pytest.FixtureRequest, tz_info: dt.tzinfo
|
||||
) -> dt.datetime | None:
|
||||
"""Return localized test time based."""
|
||||
if not hasattr(request, "param"):
|
||||
return None
|
||||
|
||||
return request.param.replace(tzinfo=tz_info)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def results(request: pytest.FixtureRequest, tz_info: dt.tzinfo) -> Iterable:
|
||||
"""Return localized results."""
|
||||
if not hasattr(request, "param"):
|
||||
return None
|
||||
|
||||
if isinstance(request.param, dict):
|
||||
return {
|
||||
key: value.replace(tzinfo=tz_info)
|
||||
if isinstance(value, dt.datetime)
|
||||
else value
|
||||
for key, value in request.param.items()
|
||||
}
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def havdalah_offset() -> int | None:
|
||||
"""Return None if default havdalah offset is not specified."""
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def language() -> str:
|
||||
"""Return default language value, unless language is parametrized."""
|
||||
return "english"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_hass(hass: HomeAssistant, location_data: _LocationData | None) -> None:
|
||||
"""Set up Home Assistant for testing the jewish_calendar integration."""
|
||||
|
||||
if location_data:
|
||||
await hass.config.async_set_time_zone(location_data.timezone)
|
||||
hass.config.latitude = location_data.lat
|
||||
hass.config.longitude = location_data.lng
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_entry(
|
||||
location_data: _LocationData | None,
|
||||
language: str,
|
||||
havdalah_offset: int | None,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the jewish_calendar integration for testing."""
|
||||
param_data = {}
|
||||
param_options = {}
|
||||
|
||||
if location_data:
|
||||
param_data = {
|
||||
CONF_DIASPORA: location_data.diaspora,
|
||||
CONF_TIME_ZONE: location_data.timezone,
|
||||
}
|
||||
param_options[CONF_CANDLE_LIGHT_MINUTES] = location_data.candle_lighting
|
||||
|
||||
if havdalah_offset:
|
||||
param_options[CONF_HAVDALAH_OFFSET_MINUTES] = havdalah_offset
|
||||
|
||||
return MockConfigEntry(
|
||||
title=DEFAULT_NAME,
|
||||
domain=DOMAIN,
|
||||
data={CONF_LANGUAGE: language, **param_data},
|
||||
options=param_options,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_at_time(
|
||||
test_time: dt.datetime, hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> AsyncGenerator[None]:
|
||||
"""Set up the jewish_calendar integration at a specific time."""
|
||||
with freeze_time(test_time):
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
yield
|
||||
|
@ -1,301 +1,145 @@
|
||||
"""The tests for the Jewish calendar binary sensors."""
|
||||
|
||||
from datetime import datetime as dt, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from freezegun import freeze_time
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.jewish_calendar.const import (
|
||||
CONF_CANDLE_LIGHT_MINUTES,
|
||||
CONF_DIASPORA,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_LANGUAGE, CONF_PLATFORM, STATE_OFF, STATE_ON
|
||||
from homeassistant.components.jewish_calendar.const import DOMAIN
|
||||
from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import make_jerusalem_test_params, make_nyc_test_params
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
MELACHA_PARAMS = [
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 1, 16, 0),
|
||||
{
|
||||
"state": STATE_ON,
|
||||
"update": dt(2018, 9, 1, 20, 14),
|
||||
"new_state": STATE_OFF,
|
||||
},
|
||||
{"state": STATE_ON, "update": dt(2018, 9, 1, 20, 14), "new_state": STATE_OFF},
|
||||
id="currently_first_shabbat",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 1, 20, 21),
|
||||
{
|
||||
"state": STATE_OFF,
|
||||
"update": dt(2018, 9, 2, 6, 21),
|
||||
"new_state": STATE_OFF,
|
||||
},
|
||||
{"state": STATE_OFF, "update": dt(2018, 9, 2, 6, 21), "new_state": STATE_OFF},
|
||||
id="after_first_shabbat",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 7, 13, 1),
|
||||
{
|
||||
"state": STATE_OFF,
|
||||
"update": dt(2018, 9, 7, 19, 4),
|
||||
"new_state": STATE_ON,
|
||||
},
|
||||
{"state": STATE_OFF, "update": dt(2018, 9, 7, 19, 4), "new_state": STATE_ON},
|
||||
id="friday_upcoming_shabbat",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 8, 21, 25),
|
||||
{
|
||||
"state": STATE_OFF,
|
||||
"update": dt(2018, 9, 9, 6, 27),
|
||||
"new_state": STATE_OFF,
|
||||
},
|
||||
{"state": STATE_OFF, "update": dt(2018, 9, 9, 6, 27), "new_state": STATE_OFF},
|
||||
id="upcoming_rosh_hashana",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 9, 21, 25),
|
||||
{
|
||||
"state": STATE_ON,
|
||||
"update": dt(2018, 9, 10, 6, 28),
|
||||
"new_state": STATE_ON,
|
||||
},
|
||||
{"state": STATE_ON, "update": dt(2018, 9, 10, 6, 28), "new_state": STATE_ON},
|
||||
id="currently_rosh_hashana",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 10, 21, 25),
|
||||
{
|
||||
"state": STATE_ON,
|
||||
"update": dt(2018, 9, 11, 6, 29),
|
||||
"new_state": STATE_ON,
|
||||
},
|
||||
{"state": STATE_ON, "update": dt(2018, 9, 11, 6, 29), "new_state": STATE_ON},
|
||||
id="second_day_rosh_hashana_night",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 11, 11, 25),
|
||||
{
|
||||
"state": STATE_ON,
|
||||
"update": dt(2018, 9, 11, 19, 57),
|
||||
"new_state": STATE_OFF,
|
||||
},
|
||||
{"state": STATE_ON, "update": dt(2018, 9, 11, 19, 57), "new_state": STATE_OFF},
|
||||
id="second_day_rosh_hashana_day",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 29, 16, 25),
|
||||
{
|
||||
"state": STATE_ON,
|
||||
"update": dt(2018, 9, 29, 19, 25),
|
||||
"new_state": STATE_OFF,
|
||||
},
|
||||
{"state": STATE_ON, "update": dt(2018, 9, 29, 19, 25), "new_state": STATE_OFF},
|
||||
id="currently_shabbat_chol_hamoed",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 29, 21, 25),
|
||||
{
|
||||
"state": STATE_OFF,
|
||||
"update": dt(2018, 9, 30, 6, 48),
|
||||
"new_state": STATE_OFF,
|
||||
},
|
||||
{"state": STATE_OFF, "update": dt(2018, 9, 30, 6, 48), "new_state": STATE_OFF},
|
||||
id="upcoming_two_day_yomtov_in_diaspora",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 30, 21, 25),
|
||||
{
|
||||
"state": STATE_ON,
|
||||
"update": dt(2018, 10, 1, 6, 49),
|
||||
"new_state": STATE_ON,
|
||||
},
|
||||
{"state": STATE_ON, "update": dt(2018, 10, 1, 6, 49), "new_state": STATE_ON},
|
||||
id="currently_first_day_of_two_day_yomtov_in_diaspora",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 10, 1, 21, 25),
|
||||
{
|
||||
"state": STATE_ON,
|
||||
"update": dt(2018, 10, 2, 6, 50),
|
||||
"new_state": STATE_ON,
|
||||
},
|
||||
{"state": STATE_ON, "update": dt(2018, 10, 2, 6, 50), "new_state": STATE_ON},
|
||||
id="currently_second_day_of_two_day_yomtov_in_diaspora",
|
||||
),
|
||||
make_jerusalem_test_params(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 9, 29, 21, 25),
|
||||
{
|
||||
"state": STATE_OFF,
|
||||
"update": dt(2018, 9, 30, 6, 29),
|
||||
"new_state": STATE_OFF,
|
||||
},
|
||||
{"state": STATE_OFF, "update": dt(2018, 9, 30, 6, 29), "new_state": STATE_OFF},
|
||||
id="upcoming_one_day_yom_tov_in_israel",
|
||||
),
|
||||
make_jerusalem_test_params(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 10, 1, 11, 25),
|
||||
{
|
||||
"state": STATE_ON,
|
||||
"update": dt(2018, 10, 1, 19, 2),
|
||||
"new_state": STATE_OFF,
|
||||
},
|
||||
{"state": STATE_ON, "update": dt(2018, 10, 1, 19, 2), "new_state": STATE_OFF},
|
||||
id="currently_one_day_yom_tov_in_israel",
|
||||
),
|
||||
make_jerusalem_test_params(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 10, 1, 21, 25),
|
||||
{
|
||||
"state": STATE_OFF,
|
||||
"update": dt(2018, 10, 2, 6, 31),
|
||||
"new_state": STATE_OFF,
|
||||
},
|
||||
{"state": STATE_OFF, "update": dt(2018, 10, 2, 6, 31), "new_state": STATE_OFF},
|
||||
id="after_one_day_yom_tov_in_israel",
|
||||
),
|
||||
]
|
||||
|
||||
MELACHA_TEST_IDS = [
|
||||
"currently_first_shabbat",
|
||||
"after_first_shabbat",
|
||||
"friday_upcoming_shabbat",
|
||||
"upcoming_rosh_hashana",
|
||||
"currently_rosh_hashana",
|
||||
"second_day_rosh_hashana_night",
|
||||
"second_day_rosh_hashana_day",
|
||||
"currently_shabbat_chol_hamoed",
|
||||
"upcoming_two_day_yomtov_in_diaspora",
|
||||
"currently_first_day_of_two_day_yomtov_in_diaspora",
|
||||
"currently_second_day_of_two_day_yomtov_in_diaspora",
|
||||
"upcoming_one_day_yom_tov_in_israel",
|
||||
"currently_one_day_yom_tov_in_israel",
|
||||
"after_one_day_yom_tov_in_israel",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"now",
|
||||
"candle_lighting",
|
||||
"havdalah",
|
||||
"diaspora",
|
||||
"tzname",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"result",
|
||||
),
|
||||
MELACHA_PARAMS,
|
||||
ids=MELACHA_TEST_IDS,
|
||||
("location_data", "test_time", "results"), MELACHA_PARAMS, indirect=True
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_at_time")
|
||||
async def test_issur_melacha_sensor(
|
||||
hass: HomeAssistant,
|
||||
now,
|
||||
candle_lighting,
|
||||
havdalah,
|
||||
diaspora,
|
||||
tzname,
|
||||
latitude,
|
||||
longitude,
|
||||
result,
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, results: dict[str, Any]
|
||||
) -> None:
|
||||
"""Test Issur Melacha sensor output."""
|
||||
time_zone = dt_util.get_time_zone(tzname)
|
||||
test_time = now.replace(tzinfo=time_zone)
|
||||
sensor_id = "binary_sensor.jewish_calendar_issur_melacha_in_effect"
|
||||
assert hass.states.get(sensor_id).state == results["state"]
|
||||
|
||||
await hass.config.async_set_time_zone(tzname)
|
||||
hass.config.latitude = latitude
|
||||
hass.config.longitude = longitude
|
||||
|
||||
with freeze_time(test_time):
|
||||
entry = MockConfigEntry(
|
||||
title=DEFAULT_NAME,
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_LANGUAGE: "english",
|
||||
CONF_DIASPORA: diaspora,
|
||||
CONF_CANDLE_LIGHT_MINUTES: candle_lighting,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES: havdalah,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass.states.get(
|
||||
"binary_sensor.jewish_calendar_issur_melacha_in_effect"
|
||||
).state
|
||||
== result["state"]
|
||||
)
|
||||
|
||||
with freeze_time(result["update"]):
|
||||
async_fire_time_changed(hass, result["update"])
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
hass.states.get(
|
||||
"binary_sensor.jewish_calendar_issur_melacha_in_effect"
|
||||
).state
|
||||
== result["new_state"]
|
||||
)
|
||||
freezer.move_to(results["update"])
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(sensor_id).state == results["new_state"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"now",
|
||||
"candle_lighting",
|
||||
"havdalah",
|
||||
"diaspora",
|
||||
"tzname",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"result",
|
||||
),
|
||||
("location_data", "test_time", "results"),
|
||||
[
|
||||
make_nyc_test_params(
|
||||
dt(2020, 10, 23, 17, 44, 59, 999999), [STATE_OFF, STATE_ON]
|
||||
),
|
||||
make_nyc_test_params(
|
||||
dt(2020, 10, 24, 18, 42, 59, 999999), [STATE_ON, STATE_OFF]
|
||||
),
|
||||
("New York", dt(2020, 10, 23, 17, 44, 59, 999999), [STATE_OFF, STATE_ON]),
|
||||
("New York", dt(2020, 10, 24, 18, 42, 59, 999999), [STATE_ON, STATE_OFF]),
|
||||
],
|
||||
ids=["before_candle_lighting", "before_havdalah"],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_at_time")
|
||||
async def test_issur_melacha_sensor_update(
|
||||
hass: HomeAssistant,
|
||||
now,
|
||||
candle_lighting,
|
||||
havdalah,
|
||||
diaspora,
|
||||
tzname,
|
||||
latitude,
|
||||
longitude,
|
||||
result,
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, results: list[str]
|
||||
) -> None:
|
||||
"""Test Issur Melacha sensor output."""
|
||||
time_zone = dt_util.get_time_zone(tzname)
|
||||
test_time = now.replace(tzinfo=time_zone)
|
||||
sensor_id = "binary_sensor.jewish_calendar_issur_melacha_in_effect"
|
||||
assert hass.states.get(sensor_id).state == results[0]
|
||||
|
||||
await hass.config.async_set_time_zone(tzname)
|
||||
hass.config.latitude = latitude
|
||||
hass.config.longitude = longitude
|
||||
|
||||
with freeze_time(test_time):
|
||||
entry = MockConfigEntry(
|
||||
title=DEFAULT_NAME,
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_LANGUAGE: "english",
|
||||
CONF_DIASPORA: diaspora,
|
||||
CONF_CANDLE_LIGHT_MINUTES: candle_lighting,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES: havdalah,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
hass.states.get(
|
||||
"binary_sensor.jewish_calendar_issur_melacha_in_effect"
|
||||
).state
|
||||
== result[0]
|
||||
)
|
||||
|
||||
test_time += timedelta(microseconds=1)
|
||||
with freeze_time(test_time):
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
hass.states.get(
|
||||
"binary_sensor.jewish_calendar_issur_melacha_in_effect"
|
||||
).state
|
||||
== result[1]
|
||||
)
|
||||
freezer.tick(timedelta(microseconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(sensor_id).state == results[1]
|
||||
|
||||
|
||||
async def test_no_discovery_info(
|
||||
|
@ -57,10 +57,10 @@ async def test_step_user(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No
|
||||
|
||||
async def test_single_instance_allowed(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test we abort if already setup."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
@ -70,11 +70,11 @@ async def test_single_instance_allowed(
|
||||
assert result.get("reason") == "single_instance_allowed"
|
||||
|
||||
|
||||
async def test_options(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None:
|
||||
async def test_options(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Test updating options."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
@ -95,16 +95,16 @@ async def test_options(hass: HomeAssistant, mock_config_entry: MockConfigEntry)
|
||||
|
||||
|
||||
async def test_options_reconfigure(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that updating the options of the Jewish Calendar integration triggers a value update."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert CONF_CANDLE_LIGHT_MINUTES not in mock_config_entry.options
|
||||
assert CONF_CANDLE_LIGHT_MINUTES not in config_entry.options
|
||||
|
||||
# Update the CONF_CANDLE_LIGHT_MINUTES option to a new value
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
@ -114,21 +114,17 @@ async def test_options_reconfigure(
|
||||
assert result["result"]
|
||||
|
||||
# The value of the "upcoming_shabbat_candle_lighting" sensor should be the new value
|
||||
assert (
|
||||
mock_config_entry.options[CONF_CANDLE_LIGHT_MINUTES] == DEFAULT_CANDLE_LIGHT + 1
|
||||
)
|
||||
assert config_entry.options[CONF_CANDLE_LIGHT_MINUTES] == DEFAULT_CANDLE_LIGHT + 1
|
||||
|
||||
|
||||
async def test_reconfigure(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
async def test_reconfigure(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Test starting a reconfigure flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# init user flow
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
result = await config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
@ -141,4 +137,4 @@ async def test_reconfigure(
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data[CONF_DIASPORA] is not DEFAULT_DIASPORA
|
||||
assert config_entry.data[CONF_DIASPORA] is not DEFAULT_DIASPORA
|
||||
|
@ -21,24 +21,24 @@ from tests.common import MockConfigEntry
|
||||
async def test_migrate_unique_id(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
old_key: str,
|
||||
new_key: str,
|
||||
) -> None:
|
||||
"""Test unique id migration."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={})
|
||||
entry.add_to_hass(hass)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
entity: er.RegistryEntry = entity_registry.async_get_or_create(
|
||||
domain=SENSOR_DOMAIN,
|
||||
platform=DOMAIN,
|
||||
unique_id=f"{entry.entry_id}-{old_key}",
|
||||
config_entry=entry,
|
||||
unique_id=f"{config_entry.entry_id}-{old_key}",
|
||||
config_entry=config_entry,
|
||||
)
|
||||
assert entity.unique_id.endswith(f"-{old_key}")
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_migrated = entity_registry.async_get(entity.entity_id)
|
||||
assert entity_migrated
|
||||
assert entity_migrated.unique_id == f"{entry.entry_id}-{new_key}"
|
||||
assert entity_migrated.unique_id == f"{config_entry.entry_id}-{new_key}"
|
||||
|
@ -1,282 +1,184 @@
|
||||
"""The tests for the Jewish calendar sensors."""
|
||||
|
||||
from datetime import datetime as dt, timedelta
|
||||
from datetime import datetime as dt
|
||||
from typing import Any
|
||||
|
||||
from freezegun import freeze_time
|
||||
from hdate.holidays import HolidayDatabase
|
||||
from hdate.parasha import Parasha
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.jewish_calendar.const import (
|
||||
CONF_CANDLE_LIGHT_MINUTES,
|
||||
CONF_DIASPORA,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.jewish_calendar.const import DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_LANGUAGE, CONF_PLATFORM
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import make_jerusalem_test_params, make_nyc_test_params
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_jewish_calendar_min_config(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize("language", ["english", "hebrew"])
|
||||
async def test_min_config(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Test minimum jewish calendar configuration."""
|
||||
entry = MockConfigEntry(title=DEFAULT_NAME, domain=DOMAIN, data={})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("sensor.jewish_calendar_date") is not None
|
||||
|
||||
|
||||
async def test_jewish_calendar_hebrew(hass: HomeAssistant) -> None:
|
||||
"""Test jewish calendar sensor with language set to hebrew."""
|
||||
entry = MockConfigEntry(
|
||||
title=DEFAULT_NAME, domain=DOMAIN, data={"language": "hebrew"}
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("sensor.jewish_calendar_date") is not None
|
||||
|
||||
|
||||
TEST_PARAMS = [
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 9, 3),
|
||||
"UTC",
|
||||
31.778,
|
||||
35.235,
|
||||
{"state": "23 Elul 5778"},
|
||||
"english",
|
||||
"date",
|
||||
False,
|
||||
"23 Elul 5778",
|
||||
None,
|
||||
id="date_output",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 9, 3),
|
||||
"UTC",
|
||||
31.778,
|
||||
35.235,
|
||||
{"state": 'כ"ג אלול ה\' תשע"ח'},
|
||||
"hebrew",
|
||||
"date",
|
||||
False,
|
||||
'כ"ג אלול ה\' תשע"ח',
|
||||
None,
|
||||
id="date_output_hebrew",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 9, 10),
|
||||
"UTC",
|
||||
31.778,
|
||||
35.235,
|
||||
{"state": "א' ראש השנה"},
|
||||
"hebrew",
|
||||
"holiday",
|
||||
False,
|
||||
"א' ראש השנה",
|
||||
None,
|
||||
id="holiday",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 9, 10),
|
||||
"UTC",
|
||||
31.778,
|
||||
35.235,
|
||||
"english",
|
||||
"holiday",
|
||||
False,
|
||||
"Rosh Hashana I",
|
||||
{
|
||||
"device_class": "enum",
|
||||
"friendly_name": "Jewish Calendar Holiday",
|
||||
"icon": "mdi:calendar-star",
|
||||
"id": "rosh_hashana_i",
|
||||
"type": "YOM_TOV",
|
||||
"options": HolidayDatabase(False).get_all_names("english"),
|
||||
"state": "Rosh Hashana I",
|
||||
"attr": {
|
||||
"device_class": "enum",
|
||||
"friendly_name": "Jewish Calendar Holiday",
|
||||
"icon": "mdi:calendar-star",
|
||||
"id": "rosh_hashana_i",
|
||||
"type": "YOM_TOV",
|
||||
"options": HolidayDatabase(False).get_all_names("english"),
|
||||
},
|
||||
},
|
||||
"english",
|
||||
"holiday",
|
||||
id="holiday_english",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2024, 12, 31),
|
||||
"UTC",
|
||||
31.778,
|
||||
35.235,
|
||||
{
|
||||
"state": "Chanukah, Rosh Chodesh",
|
||||
"attr": {
|
||||
"device_class": "enum",
|
||||
"friendly_name": "Jewish Calendar Holiday",
|
||||
"icon": "mdi:calendar-star",
|
||||
"id": "chanukah, rosh_chodesh",
|
||||
"type": "MELACHA_PERMITTED_HOLIDAY, ROSH_CHODESH",
|
||||
"options": HolidayDatabase(False).get_all_names("english"),
|
||||
},
|
||||
},
|
||||
"english",
|
||||
"holiday",
|
||||
False,
|
||||
"Chanukah, Rosh Chodesh",
|
||||
{
|
||||
"device_class": "enum",
|
||||
"friendly_name": "Jewish Calendar Holiday",
|
||||
"icon": "mdi:calendar-star",
|
||||
"id": "chanukah, rosh_chodesh",
|
||||
"type": "MELACHA_PERMITTED_HOLIDAY, ROSH_CHODESH",
|
||||
"options": HolidayDatabase(False).get_all_names("english"),
|
||||
},
|
||||
id="holiday_multiple",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 9, 8),
|
||||
"UTC",
|
||||
31.778,
|
||||
35.235,
|
||||
{
|
||||
"state": "נצבים",
|
||||
"attr": {
|
||||
"device_class": "enum",
|
||||
"friendly_name": "Jewish Calendar Parshat Hashavua",
|
||||
"icon": "mdi:book-open-variant",
|
||||
"options": list(Parasha),
|
||||
},
|
||||
},
|
||||
"hebrew",
|
||||
"parshat_hashavua",
|
||||
False,
|
||||
"נצבים",
|
||||
{
|
||||
"device_class": "enum",
|
||||
"friendly_name": "Jewish Calendar Parshat Hashavua",
|
||||
"icon": "mdi:book-open-variant",
|
||||
"options": list(Parasha),
|
||||
},
|
||||
id="torah_reading",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 8),
|
||||
"America/New_York",
|
||||
40.7128,
|
||||
-74.0060,
|
||||
{"state": dt(2018, 9, 8, 19, 47)},
|
||||
"hebrew",
|
||||
"t_set_hakochavim",
|
||||
True,
|
||||
dt(2018, 9, 8, 19, 47),
|
||||
None,
|
||||
id="first_stars_ny",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 9, 8),
|
||||
"Asia/Jerusalem",
|
||||
31.778,
|
||||
35.235,
|
||||
{"state": dt(2018, 9, 8, 19, 21)},
|
||||
"hebrew",
|
||||
"t_set_hakochavim",
|
||||
False,
|
||||
dt(2018, 9, 8, 19, 21),
|
||||
None,
|
||||
id="first_stars_jerusalem",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 10, 14),
|
||||
"Asia/Jerusalem",
|
||||
31.778,
|
||||
35.235,
|
||||
{"state": "לך לך"},
|
||||
"hebrew",
|
||||
"parshat_hashavua",
|
||||
False,
|
||||
"לך לך",
|
||||
None,
|
||||
id="torah_reading_weekday",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 10, 14, 17, 0, 0),
|
||||
"Asia/Jerusalem",
|
||||
31.778,
|
||||
35.235,
|
||||
{"state": "ה' מרחשוון ה' תשע\"ט"},
|
||||
"hebrew",
|
||||
"date",
|
||||
False,
|
||||
"ה' מרחשוון ה' תשע\"ט",
|
||||
None,
|
||||
id="date_before_sunset",
|
||||
),
|
||||
(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 10, 14, 19, 0, 0),
|
||||
"Asia/Jerusalem",
|
||||
31.778,
|
||||
35.235,
|
||||
{
|
||||
"state": "ו' מרחשוון ה' תשע\"ט",
|
||||
"attr": {
|
||||
"hebrew_year": "5779",
|
||||
"hebrew_month_name": "מרחשוון",
|
||||
"hebrew_day": "6",
|
||||
"icon": "mdi:star-david",
|
||||
"friendly_name": "Jewish Calendar Date",
|
||||
},
|
||||
},
|
||||
"hebrew",
|
||||
"date",
|
||||
False,
|
||||
"ו' מרחשוון ה' תשע\"ט",
|
||||
{
|
||||
"hebrew_year": "5779",
|
||||
"hebrew_month_name": "מרחשוון",
|
||||
"hebrew_day": "6",
|
||||
"icon": "mdi:star-david",
|
||||
"friendly_name": "Jewish Calendar Date",
|
||||
},
|
||||
id="date_after_sunset",
|
||||
),
|
||||
]
|
||||
|
||||
TEST_IDS = [
|
||||
"date_output",
|
||||
"date_output_hebrew",
|
||||
"holiday",
|
||||
"holiday_english",
|
||||
"holiday_multiple",
|
||||
"torah_reading",
|
||||
"first_stars_ny",
|
||||
"first_stars_jerusalem",
|
||||
"torah_reading_weekday",
|
||||
"date_before_sunset",
|
||||
"date_after_sunset",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"now",
|
||||
"tzname",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"language",
|
||||
"sensor",
|
||||
"diaspora",
|
||||
"result",
|
||||
"attrs",
|
||||
),
|
||||
("location_data", "test_time", "results", "language", "sensor"),
|
||||
TEST_PARAMS,
|
||||
ids=TEST_IDS,
|
||||
indirect=["location_data", "test_time", "results"],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "setup_at_time")
|
||||
async def test_jewish_calendar_sensor(
|
||||
hass: HomeAssistant,
|
||||
now,
|
||||
tzname,
|
||||
latitude,
|
||||
longitude,
|
||||
language,
|
||||
sensor,
|
||||
diaspora,
|
||||
result,
|
||||
attrs,
|
||||
hass: HomeAssistant, results: dict[str, Any], sensor: str
|
||||
) -> None:
|
||||
"""Test Jewish calendar sensor output."""
|
||||
time_zone = dt_util.get_time_zone(tzname)
|
||||
test_time = now.replace(tzinfo=time_zone)
|
||||
|
||||
await hass.config.async_set_time_zone(tzname)
|
||||
hass.config.latitude = latitude
|
||||
hass.config.longitude = longitude
|
||||
|
||||
with freeze_time(test_time):
|
||||
entry = MockConfigEntry(
|
||||
title=DEFAULT_NAME,
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_LANGUAGE: language,
|
||||
CONF_DIASPORA: diaspora,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
future = test_time + timedelta(seconds=30)
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = (
|
||||
dt_util.as_utc(result.replace(tzinfo=time_zone)).isoformat()
|
||||
if isinstance(result, dt)
|
||||
else result
|
||||
)
|
||||
result = results["state"]
|
||||
if isinstance(result, dt):
|
||||
result = dt_util.as_utc(result).isoformat()
|
||||
|
||||
sensor_object = hass.states.get(f"sensor.jewish_calendar_{sensor}")
|
||||
assert sensor_object.state == result
|
||||
|
||||
if attrs:
|
||||
if attrs := results.get("attr"):
|
||||
assert sensor_object.attributes == attrs
|
||||
|
||||
|
||||
SHABBAT_PARAMS = [
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 1, 16, 0),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 12),
|
||||
@ -286,8 +188,11 @@ SHABBAT_PARAMS = [
|
||||
"english_parshat_hashavua": "Ki Tavo",
|
||||
"hebrew_parshat_hashavua": "כי תבוא",
|
||||
},
|
||||
None,
|
||||
id="currently_first_shabbat",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 1, 16, 0),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 12),
|
||||
@ -297,9 +202,11 @@ SHABBAT_PARAMS = [
|
||||
"english_parshat_hashavua": "Ki Tavo",
|
||||
"hebrew_parshat_hashavua": "כי תבוא",
|
||||
},
|
||||
havdalah_offset=50,
|
||||
50, # Havdalah offset
|
||||
id="currently_first_shabbat_with_havdalah_offset",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 1, 20, 0),
|
||||
{
|
||||
"english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 12),
|
||||
@ -309,8 +216,11 @@ SHABBAT_PARAMS = [
|
||||
"english_parshat_hashavua": "Ki Tavo",
|
||||
"hebrew_parshat_hashavua": "כי תבוא",
|
||||
},
|
||||
None,
|
||||
id="currently_first_shabbat_bein_hashmashot_lagging_date",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 1, 20, 21),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 7, 19),
|
||||
@ -320,8 +230,11 @@ SHABBAT_PARAMS = [
|
||||
"english_parshat_hashavua": "Nitzavim",
|
||||
"hebrew_parshat_hashavua": "נצבים",
|
||||
},
|
||||
None,
|
||||
id="after_first_shabbat",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 7, 13, 1),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 7, 19),
|
||||
@ -331,8 +244,11 @@ SHABBAT_PARAMS = [
|
||||
"english_parshat_hashavua": "Nitzavim",
|
||||
"hebrew_parshat_hashavua": "נצבים",
|
||||
},
|
||||
None,
|
||||
id="friday_upcoming_shabbat",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 8, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 9, 18, 57),
|
||||
@ -344,8 +260,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Erev Rosh Hashana",
|
||||
"hebrew_holiday": "ערב ראש השנה",
|
||||
},
|
||||
None,
|
||||
id="upcoming_rosh_hashana",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 9, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 9, 18, 57),
|
||||
@ -357,8 +276,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Rosh Hashana I",
|
||||
"hebrew_holiday": "א' ראש השנה",
|
||||
},
|
||||
None,
|
||||
id="currently_rosh_hashana",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 10, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 9, 18, 57),
|
||||
@ -370,8 +292,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Rosh Hashana II",
|
||||
"hebrew_holiday": "ב' ראש השנה",
|
||||
},
|
||||
None,
|
||||
id="second_day_rosh_hashana",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 28, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 28, 18, 25),
|
||||
@ -381,8 +306,11 @@ SHABBAT_PARAMS = [
|
||||
"english_parshat_hashavua": "none",
|
||||
"hebrew_parshat_hashavua": "none",
|
||||
},
|
||||
None,
|
||||
id="currently_shabbat_chol_hamoed",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 29, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 22),
|
||||
@ -394,8 +322,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Hoshana Raba",
|
||||
"hebrew_holiday": "הושענא רבה",
|
||||
},
|
||||
None,
|
||||
id="upcoming_two_day_yomtov_in_diaspora",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 9, 30, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 22),
|
||||
@ -407,8 +338,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Shmini Atzeret",
|
||||
"hebrew_holiday": "שמיני עצרת",
|
||||
},
|
||||
None,
|
||||
id="currently_first_day_of_two_day_yomtov_in_diaspora",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2018, 10, 1, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 22),
|
||||
@ -420,11 +354,14 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Simchat Torah",
|
||||
"hebrew_holiday": "שמחת תורה",
|
||||
},
|
||||
None,
|
||||
id="currently_second_day_of_two_day_yomtov_in_diaspora",
|
||||
),
|
||||
make_jerusalem_test_params(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 9, 29, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 30, 17, 45),
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 30, 17, 46),
|
||||
"english_upcoming_havdalah": dt(2018, 10, 1, 19, 1),
|
||||
"english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 17, 39),
|
||||
"english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 54),
|
||||
@ -433,11 +370,14 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Hoshana Raba",
|
||||
"hebrew_holiday": "הושענא רבה",
|
||||
},
|
||||
None,
|
||||
id="upcoming_one_day_yom_tov_in_israel",
|
||||
),
|
||||
make_jerusalem_test_params(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 9, 30, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 30, 17, 45),
|
||||
"english_upcoming_candle_lighting": dt(2018, 9, 30, 17, 46),
|
||||
"english_upcoming_havdalah": dt(2018, 10, 1, 19, 1),
|
||||
"english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 17, 39),
|
||||
"english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 54),
|
||||
@ -446,8 +386,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Shmini Atzeret, Simchat Torah",
|
||||
"hebrew_holiday": "שמיני עצרת, שמחת תורה",
|
||||
},
|
||||
None,
|
||||
id="currently_one_day_yom_tov_in_israel",
|
||||
),
|
||||
make_jerusalem_test_params(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2018, 10, 1, 21, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2018, 10, 5, 17, 39),
|
||||
@ -457,8 +400,11 @@ SHABBAT_PARAMS = [
|
||||
"english_parshat_hashavua": "Bereshit",
|
||||
"hebrew_parshat_hashavua": "בראשית",
|
||||
},
|
||||
None,
|
||||
id="after_one_day_yom_tov_in_israel",
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2016, 6, 11, 8, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 9),
|
||||
@ -470,8 +416,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Erev Shavuot",
|
||||
"hebrew_holiday": "ערב שבועות",
|
||||
},
|
||||
None,
|
||||
id="currently_first_day_of_three_day_type1_yomtov_in_diaspora", # Type 1 = Sat/Sun/Mon
|
||||
),
|
||||
make_nyc_test_params(
|
||||
pytest.param(
|
||||
"New York",
|
||||
dt(2016, 6, 12, 8, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 9),
|
||||
@ -483,8 +432,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Shavuot",
|
||||
"hebrew_holiday": "שבועות",
|
||||
},
|
||||
None,
|
||||
id="currently_second_day_of_three_day_type1_yomtov_in_diaspora", # Type 1 = Sat/Sun/Mon
|
||||
),
|
||||
make_jerusalem_test_params(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2017, 9, 21, 8, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2017, 9, 20, 17, 58),
|
||||
@ -496,8 +448,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Rosh Hashana I",
|
||||
"hebrew_holiday": "א' ראש השנה",
|
||||
},
|
||||
None,
|
||||
id="currently_first_day_of_three_day_type2_yomtov_in_israel", # Type 2 = Thurs/Fri/Sat
|
||||
),
|
||||
make_jerusalem_test_params(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2017, 9, 22, 8, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2017, 9, 20, 17, 58),
|
||||
@ -509,8 +464,11 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "Rosh Hashana II",
|
||||
"hebrew_holiday": "ב' ראש השנה",
|
||||
},
|
||||
None,
|
||||
id="currently_second_day_of_three_day_type2_yomtov_in_israel", # Type 2 = Thurs/Fri/Sat
|
||||
),
|
||||
make_jerusalem_test_params(
|
||||
pytest.param(
|
||||
"Jerusalem",
|
||||
dt(2017, 9, 23, 8, 25),
|
||||
{
|
||||
"english_upcoming_candle_lighting": dt(2017, 9, 20, 17, 58),
|
||||
@ -522,179 +480,70 @@ SHABBAT_PARAMS = [
|
||||
"english_holiday": "",
|
||||
"hebrew_holiday": "",
|
||||
},
|
||||
None,
|
||||
id="currently_third_day_of_three_day_type2_yomtov_in_israel", # Type 2 = Thurs/Fri/Sat
|
||||
),
|
||||
]
|
||||
|
||||
SHABBAT_TEST_IDS = [
|
||||
"currently_first_shabbat",
|
||||
"currently_first_shabbat_with_havdalah_offset",
|
||||
"currently_first_shabbat_bein_hashmashot_lagging_date",
|
||||
"after_first_shabbat",
|
||||
"friday_upcoming_shabbat",
|
||||
"upcoming_rosh_hashana",
|
||||
"currently_rosh_hashana",
|
||||
"second_day_rosh_hashana",
|
||||
"currently_shabbat_chol_hamoed",
|
||||
"upcoming_two_day_yomtov_in_diaspora",
|
||||
"currently_first_day_of_two_day_yomtov_in_diaspora",
|
||||
"currently_second_day_of_two_day_yomtov_in_diaspora",
|
||||
"upcoming_one_day_yom_tov_in_israel",
|
||||
"currently_one_day_yom_tov_in_israel",
|
||||
"after_one_day_yom_tov_in_israel",
|
||||
# Type 1 = Sat/Sun/Mon
|
||||
"currently_first_day_of_three_day_type1_yomtov_in_diaspora",
|
||||
"currently_second_day_of_three_day_type1_yomtov_in_diaspora",
|
||||
# Type 2 = Thurs/Fri/Sat
|
||||
"currently_first_day_of_three_day_type2_yomtov_in_israel",
|
||||
"currently_second_day_of_three_day_type2_yomtov_in_israel",
|
||||
"currently_third_day_of_three_day_type2_yomtov_in_israel",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language", ["english", "hebrew"])
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"now",
|
||||
"candle_lighting",
|
||||
"havdalah",
|
||||
"diaspora",
|
||||
"tzname",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"result",
|
||||
),
|
||||
("location_data", "test_time", "results", "havdalah_offset"),
|
||||
SHABBAT_PARAMS,
|
||||
ids=SHABBAT_TEST_IDS,
|
||||
indirect=("location_data", "test_time", "results"),
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "setup_at_time")
|
||||
async def test_shabbat_times_sensor(
|
||||
hass: HomeAssistant,
|
||||
language,
|
||||
now,
|
||||
candle_lighting,
|
||||
havdalah,
|
||||
diaspora,
|
||||
tzname,
|
||||
latitude,
|
||||
longitude,
|
||||
result,
|
||||
hass: HomeAssistant, results: dict[str, Any], language: str
|
||||
) -> None:
|
||||
"""Test sensor output for upcoming shabbat/yomtov times."""
|
||||
time_zone = dt_util.get_time_zone(tzname)
|
||||
test_time = now.replace(tzinfo=time_zone)
|
||||
|
||||
await hass.config.async_set_time_zone(tzname)
|
||||
hass.config.latitude = latitude
|
||||
hass.config.longitude = longitude
|
||||
|
||||
with freeze_time(test_time):
|
||||
entry = MockConfigEntry(
|
||||
title=DEFAULT_NAME,
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_LANGUAGE: language,
|
||||
CONF_DIASPORA: diaspora,
|
||||
},
|
||||
options={
|
||||
CONF_CANDLE_LIGHT_MINUTES: candle_lighting,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES: havdalah,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
future = test_time + timedelta(seconds=30)
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for sensor_type, result_value in result.items():
|
||||
for sensor_type, result_value in results.items():
|
||||
if not sensor_type.startswith(language):
|
||||
continue
|
||||
|
||||
sensor_type = sensor_type.replace(f"{language}_", "")
|
||||
|
||||
result_value = (
|
||||
dt_util.as_utc(result_value).isoformat()
|
||||
if isinstance(result_value, dt)
|
||||
else result_value
|
||||
)
|
||||
if isinstance(result_value, dt):
|
||||
result_value = dt_util.as_utc(result_value).isoformat()
|
||||
|
||||
assert hass.states.get(f"sensor.jewish_calendar_{sensor_type}").state == str(
|
||||
result_value
|
||||
), f"Value for {sensor_type}"
|
||||
|
||||
|
||||
OMER_PARAMS = [
|
||||
(dt(2019, 4, 21, 0), "1"),
|
||||
(dt(2019, 4, 21, 23), "2"),
|
||||
(dt(2019, 5, 23, 0), "33"),
|
||||
(dt(2019, 6, 8, 0), "49"),
|
||||
(dt(2019, 6, 9, 0), "0"),
|
||||
(dt(2019, 1, 1, 0), "0"),
|
||||
]
|
||||
OMER_TEST_IDS = [
|
||||
"first_day_of_omer",
|
||||
"first_day_of_omer_after_tzeit",
|
||||
"lag_baomer",
|
||||
"last_day_of_omer",
|
||||
"shavuot_no_omer",
|
||||
"jan_1st_no_omer",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("test_time", "result"), OMER_PARAMS, ids=OMER_TEST_IDS)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_omer_sensor(hass: HomeAssistant, test_time, result) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("test_time", "results"),
|
||||
[
|
||||
pytest.param(dt(2019, 4, 21, 0), "1", id="first_day_of_omer"),
|
||||
pytest.param(dt(2019, 4, 21, 23), "2", id="first_day_of_omer_after_tzeit"),
|
||||
pytest.param(dt(2019, 5, 23, 0), "33", id="lag_baomer"),
|
||||
pytest.param(dt(2019, 6, 8, 0), "49", id="last_day_of_omer"),
|
||||
pytest.param(dt(2019, 6, 9, 0), "0", id="shavuot_no_omer"),
|
||||
pytest.param(dt(2019, 1, 1, 0), "0", id="jan_1st_no_omer"),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "setup_at_time")
|
||||
async def test_omer_sensor(hass: HomeAssistant, results: str) -> None:
|
||||
"""Test Omer Count sensor output."""
|
||||
test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone))
|
||||
|
||||
with freeze_time(test_time):
|
||||
entry = MockConfigEntry(title=DEFAULT_NAME, domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
future = test_time + timedelta(seconds=30)
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.jewish_calendar_day_of_the_omer").state == result
|
||||
assert hass.states.get("sensor.jewish_calendar_day_of_the_omer").state == results
|
||||
|
||||
|
||||
DAFYOMI_PARAMS = [
|
||||
(dt(2014, 4, 28, 0), "Beitzah 29"),
|
||||
(dt(2020, 1, 4, 0), "Niddah 73"),
|
||||
(dt(2020, 1, 5, 0), "Berachos 2"),
|
||||
(dt(2020, 3, 7, 0), "Berachos 64"),
|
||||
(dt(2020, 3, 8, 0), "Shabbos 2"),
|
||||
]
|
||||
DAFYOMI_TEST_IDS = [
|
||||
"randomly_picked_date",
|
||||
"end_of_cycle13",
|
||||
"start_of_cycle14",
|
||||
"cycle14_end_of_berachos",
|
||||
"cycle14_start_of_shabbos",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("test_time", "result"), DAFYOMI_PARAMS, ids=DAFYOMI_TEST_IDS)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_dafyomi_sensor(hass: HomeAssistant, test_time, result) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("test_time", "results"),
|
||||
[
|
||||
pytest.param(dt(2014, 4, 28, 0), "Beitzah 29", id="randomly_picked_date"),
|
||||
pytest.param(dt(2020, 1, 4, 0), "Niddah 73", id="end_of_cycle13"),
|
||||
pytest.param(dt(2020, 1, 5, 0), "Berachos 2", id="start_of_cycle14"),
|
||||
pytest.param(dt(2020, 3, 7, 0), "Berachos 64", id="cycle14_end_of_berachos"),
|
||||
pytest.param(dt(2020, 3, 8, 0), "Shabbos 2", id="cycle14_start_of_shabbos"),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "setup_at_time")
|
||||
async def test_dafyomi_sensor(hass: HomeAssistant, results: str) -> None:
|
||||
"""Test Daf Yomi sensor output."""
|
||||
test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone))
|
||||
|
||||
with freeze_time(test_time):
|
||||
entry = MockConfigEntry(title=DEFAULT_NAME, domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
future = test_time + timedelta(seconds=30)
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.jewish_calendar_daf_yomi").state == result
|
||||
assert hass.states.get("sensor.jewish_calendar_daf_yomi").state == results
|
||||
|
||||
|
||||
async def test_no_discovery_info(
|
||||
|
@ -33,15 +33,15 @@ from tests.common import MockConfigEntry
|
||||
)
|
||||
async def test_get_omer_blessing(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
config_entry: MockConfigEntry,
|
||||
test_date: dt.date,
|
||||
nusach: str,
|
||||
language: Language,
|
||||
expected: str,
|
||||
) -> None:
|
||||
"""Test get omer blessing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.services.async_call(
|
||||
|
@ -143,3 +143,51 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[binary_sensor.gs012345_websocket_connected-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'binary_sensor.gs012345_websocket_connected',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'WebSocket connected',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'websocket_connected',
|
||||
'unique_id': 'GS012345_websocket_connected',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[binary_sensor.gs012345_websocket_connected-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'connectivity',
|
||||
'friendly_name': 'GS012345 WebSocket connected',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.gs012345_websocket_connected',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
|
@ -115,3 +115,177 @@
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_prebrew_off[Linea Micra]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'MR012345 Prebrew off time',
|
||||
'max': 10,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mr012345_prebrew_off_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '5.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_prebrew_off[Linea Micra].1
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 10,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.mr012345_prebrew_off_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Prebrew off time',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'prebrew_time_off',
|
||||
'unique_id': 'MR012345_prebrew_off',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_prebrew_on[Linea Micra]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'MR012345 Prebrew on time',
|
||||
'max': 10,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mr012345_prebrew_on_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '5.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_prebrew_on[Linea Micra].1
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 10,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.mr012345_prebrew_on_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Prebrew on time',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'prebrew_time_on',
|
||||
'unique_id': 'MR012345_prebrew_on',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_preinfusion[Linea Micra]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'MR012345 Preinfusion time',
|
||||
'max': 10,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mr012345_preinfusion_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_preinfusion[Linea Micra].1
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 10,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.mr012345_preinfusion_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Preinfusion time',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'preinfusion_time',
|
||||
'unique_id': 'MR012345_preinfusion_off',
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
})
|
||||
# ---
|
||||
|
97
tests/components/lamarzocco/snapshots/test_sensor.ambr
Normal file
97
tests/components/lamarzocco/snapshots/test_sensor.ambr
Normal file
@ -0,0 +1,97 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensors[sensor.gs012345_coffee_boiler_ready_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.gs012345_coffee_boiler_ready_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Coffee boiler ready time',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'coffee_boiler_ready_time',
|
||||
'unique_id': 'GS012345_coffee_boiler_ready_time',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.gs012345_coffee_boiler_ready_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'GS012345 Coffee boiler ready time',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gs012345_coffee_boiler_ready_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.gs012345_steam_boiler_ready_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.gs012345_steam_boiler_ready_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Steam boiler ready time',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'steam_boiler_ready_time',
|
||||
'unique_id': 'GS012345_steam_boiler_ready_time',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.gs012345_steam_boiler_ready_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'GS012345 Steam boiler ready time',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.gs012345_steam_boiler_ready_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
@ -27,7 +27,7 @@
|
||||
'original_name': 'Gateway firmware',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <UpdateEntityFeature: 1>,
|
||||
'supported_features': <UpdateEntityFeature: 21>,
|
||||
'translation_key': 'gateway_firmware',
|
||||
'unique_id': 'GS012345_gateway_firmware',
|
||||
'unit_of_measurement': None,
|
||||
@ -47,7 +47,7 @@
|
||||
'release_summary': None,
|
||||
'release_url': 'https://support-iot.lamarzocco.com/firmware-updates/',
|
||||
'skipped_version': None,
|
||||
'supported_features': <UpdateEntityFeature: 1>,
|
||||
'supported_features': <UpdateEntityFeature: 21>,
|
||||
'title': None,
|
||||
'update_percentage': None,
|
||||
}),
|
||||
@ -87,7 +87,7 @@
|
||||
'original_name': 'Machine firmware',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <UpdateEntityFeature: 1>,
|
||||
'supported_features': <UpdateEntityFeature: 21>,
|
||||
'translation_key': 'machine_firmware',
|
||||
'unique_id': 'GS012345_machine_firmware',
|
||||
'unit_of_measurement': None,
|
||||
@ -107,7 +107,7 @@
|
||||
'release_summary': None,
|
||||
'release_url': 'https://support-iot.lamarzocco.com/firmware-updates/',
|
||||
'skipped_version': None,
|
||||
'supported_features': <UpdateEntityFeature: 1>,
|
||||
'supported_features': <UpdateEntityFeature: 21>,
|
||||
'title': None,
|
||||
'update_percentage': None,
|
||||
}),
|
||||
|
@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pylamarzocco.exceptions import RequestNotSuccessful
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
@ -16,6 +17,7 @@ from . import async_init_integration
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
@ -20,6 +20,7 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_MAC, CONF_PASSWORD, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from . import USER_INPUT, async_init_integration, get_bluetooth_service_info
|
||||
@ -259,6 +260,61 @@ async def test_reconfigure_flow(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"discovered",
|
||||
[
|
||||
[],
|
||||
[
|
||||
BluetoothServiceInfo(
|
||||
name="SomeDevice",
|
||||
address="aa:bb:cc:dd:ee:ff",
|
||||
rssi=-63,
|
||||
manufacturer_data={},
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
)
|
||||
],
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_flow_no_machines(
|
||||
hass: HomeAssistant,
|
||||
mock_cloud_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
discovered: list[BluetoothServiceInfo],
|
||||
) -> None:
|
||||
"""Testing reconfgure flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
data = deepcopy(dict(mock_config_entry.data))
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
result = await __do_successful_user_step(hass, result, mock_cloud_client)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.lamarzocco.config_flow.async_discovered_service_info",
|
||||
return_value=discovered,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_MACHINE: "GS012345",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
assert mock_config_entry.title == "My LaMarzocco"
|
||||
assert CONF_MAC not in mock_config_entry.data
|
||||
assert dict(mock_config_entry.data) == data
|
||||
|
||||
|
||||
async def test_bluetooth_discovery(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
|
@ -3,7 +3,12 @@
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pylamarzocco.const import SmartStandByType
|
||||
from pylamarzocco.const import (
|
||||
ModelName,
|
||||
PreExtractionMode,
|
||||
SmartStandByType,
|
||||
WidgetType,
|
||||
)
|
||||
from pylamarzocco.exceptions import RequestNotSuccessful
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
@ -85,6 +90,140 @@ async def test_general_numbers(
|
||||
mock_func.assert_called_once_with(**kwargs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MICRA])
|
||||
async def test_preinfusion(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test preinfusion number."""
|
||||
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
serial_number = mock_lamarzocco.serial_number
|
||||
entity_id = f"number.{serial_number}_preinfusion_time"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
assert state
|
||||
assert state == snapshot
|
||||
|
||||
entry = entity_registry.async_get(state.entity_id)
|
||||
assert entry
|
||||
assert entry.device_id
|
||||
assert entry == snapshot
|
||||
|
||||
# service call
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_VALUE: 5.3,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_lamarzocco.set_pre_extraction_times.assert_called_once_with(
|
||||
seconds_off=5.3,
|
||||
seconds_on=0,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MICRA])
|
||||
async def test_prebrew_on(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test prebrew on number."""
|
||||
|
||||
mock_lamarzocco.dashboard.config[
|
||||
WidgetType.CM_PRE_BREWING
|
||||
].mode = PreExtractionMode.PREBREWING
|
||||
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
serial_number = mock_lamarzocco.serial_number
|
||||
entity_id = f"number.{serial_number}_prebrew_on_time"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
assert state
|
||||
assert state == snapshot
|
||||
|
||||
entry = entity_registry.async_get(state.entity_id)
|
||||
assert entry
|
||||
assert entry.device_id
|
||||
assert entry == snapshot
|
||||
|
||||
# service call
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_VALUE: 5.3,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_lamarzocco.set_pre_extraction_times.assert_called_once_with(
|
||||
seconds_on=5.3,
|
||||
seconds_off=mock_lamarzocco.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
.times.pre_brewing[0]
|
||||
.seconds.seconds_out,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MICRA])
|
||||
async def test_prebrew_off(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test prebrew off number."""
|
||||
mock_lamarzocco.dashboard.config[
|
||||
WidgetType.CM_PRE_BREWING
|
||||
].mode = PreExtractionMode.PREBREWING
|
||||
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
serial_number = mock_lamarzocco.serial_number
|
||||
entity_id = f"number.{serial_number}_prebrew_off_time"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
assert state
|
||||
assert state == snapshot
|
||||
|
||||
entry = entity_registry.async_get(state.entity_id)
|
||||
assert entry
|
||||
assert entry.device_id
|
||||
assert entry == snapshot
|
||||
|
||||
# service call
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_VALUE: 7,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_lamarzocco.set_pre_extraction_times.assert_called_once_with(
|
||||
seconds_off=7,
|
||||
seconds_on=mock_lamarzocco.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
.times.pre_brewing[0]
|
||||
.seconds.seconds_in,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_number_error(
|
||||
hass: HomeAssistant,
|
||||
|
52
tests/components/lamarzocco/test_sensor.py
Normal file
52
tests/components/lamarzocco/test_sensor.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""Tests for La Marzocco sensors."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pylamarzocco.const import ModelName
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import async_init_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the La Marzocco sensors."""
|
||||
|
||||
with patch("homeassistant.components.lamarzocco.PLATFORMS", [Platform.SENSOR]):
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_fixture",
|
||||
[ModelName.GS3_AV, ModelName.GS3_MP, ModelName.LINEA_MINI, ModelName.LINEA_MICRA],
|
||||
)
|
||||
async def test_steam_ready_entity_for_all_machines(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the La Marzocco steam ready sensor for all machines."""
|
||||
|
||||
serial_number = mock_lamarzocco.serial_number
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(f"sensor.{serial_number}_steam_boiler_ready_time")
|
||||
|
||||
assert state
|
||||
|
||||
entry = entity_registry.async_get(state.entity_id)
|
||||
assert entry
|
@ -1,8 +1,16 @@
|
||||
"""Tests for the La Marzocco Update Entities."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pylamarzocco.const import (
|
||||
FirmwareType,
|
||||
UpdateCommandStatus,
|
||||
UpdateProgressInfo,
|
||||
UpdateStatus,
|
||||
)
|
||||
from pylamarzocco.exceptions import RequestNotSuccessful
|
||||
from pylamarzocco.models import UpdateDetails
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
@ -15,6 +23,17 @@ from homeassistant.helpers import entity_registry as er
|
||||
from . import async_init_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_sleep() -> Generator[AsyncMock]:
|
||||
"""Mock asyncio.sleep."""
|
||||
with patch(
|
||||
"homeassistant.components.lamarzocco.update.asyncio.sleep",
|
||||
return_value=AsyncMock(),
|
||||
) as mock_sleep:
|
||||
yield mock_sleep
|
||||
|
||||
|
||||
async def test_update(
|
||||
@ -29,17 +48,51 @@ async def test_update(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_update_entites(
|
||||
async def test_update_process(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test the La Marzocco update entities."""
|
||||
|
||||
serial_number = mock_lamarzocco.serial_number
|
||||
|
||||
mock_lamarzocco.get_firmware.side_effect = [
|
||||
UpdateDetails(
|
||||
status=UpdateStatus.TO_UPDATE,
|
||||
command_status=UpdateCommandStatus.IN_PROGRESS,
|
||||
progress_info=UpdateProgressInfo.STARTING_PROCESS,
|
||||
progress_percentage=0,
|
||||
),
|
||||
UpdateDetails(
|
||||
status=UpdateStatus.UPDATED,
|
||||
command_status=None,
|
||||
progress_info=None,
|
||||
progress_percentage=None,
|
||||
),
|
||||
]
|
||||
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "update/release_notes",
|
||||
"entity_id": f"update.{serial_number}_gateway_firmware",
|
||||
}
|
||||
)
|
||||
result = await client.receive_json()
|
||||
assert (
|
||||
mock_lamarzocco.settings.firmwares[
|
||||
FirmwareType.GATEWAY
|
||||
].available_update.change_log
|
||||
in result["result"]
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
@ -76,3 +129,35 @@ async def test_update_error(
|
||||
blocking=True,
|
||||
)
|
||||
assert exc_info.value.translation_key == "update_failed"
|
||||
|
||||
|
||||
async def test_update_times_out(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test error during update."""
|
||||
mock_lamarzocco.get_firmware.return_value = UpdateDetails(
|
||||
status=UpdateStatus.TO_UPDATE,
|
||||
command_status=UpdateCommandStatus.IN_PROGRESS,
|
||||
progress_info=UpdateProgressInfo.STARTING_PROCESS,
|
||||
progress_percentage=0,
|
||||
)
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(f"update.{mock_lamarzocco.serial_number}_gateway_firmware")
|
||||
assert state
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.lamarzocco.update.MAX_UPDATE_WAIT", 0),
|
||||
pytest.raises(HomeAssistantError) as exc_info,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: f"update.{mock_lamarzocco.serial_number}_gateway_firmware",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert exc_info.value.translation_key == "update_failed"
|
||||
|
@ -68,7 +68,7 @@ def mock_uuid() -> Generator[AsyncMock]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_thinq_api(mock_thinq_mqtt_client: AsyncMock) -> Generator[AsyncMock]:
|
||||
def mock_config_thinq_api() -> Generator[AsyncMock]:
|
||||
"""Mock a thinq api."""
|
||||
with (
|
||||
patch("homeassistant.components.lg_thinq.ThinQApi", autospec=True) as mock_api,
|
||||
@ -77,6 +77,26 @@ def mock_thinq_api(mock_thinq_mqtt_client: AsyncMock) -> Generator[AsyncMock]:
|
||||
new=mock_api,
|
||||
),
|
||||
):
|
||||
thinq_api = mock_api.return_value
|
||||
thinq_api.async_get_device_list.return_value = ["air_conditioner"]
|
||||
yield thinq_api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_invalid_thinq_api(mock_config_thinq_api: AsyncMock) -> AsyncMock:
|
||||
"""Mock an invalid thinq api."""
|
||||
mock_config_thinq_api.async_get_device_list = AsyncMock(
|
||||
side_effect=ThinQAPIException(
|
||||
code="1309", message="Not allowed api call", headers=None
|
||||
)
|
||||
)
|
||||
return mock_config_thinq_api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_thinq_api() -> Generator[AsyncMock]:
|
||||
"""Mock a thinq api."""
|
||||
with patch("homeassistant.components.lg_thinq.ThinQApi", autospec=True) as mock_api:
|
||||
thinq_api = mock_api.return_value
|
||||
thinq_api.async_get_device_list.return_value = [
|
||||
load_json_object_fixture("air_conditioner/device.json", DOMAIN)
|
||||
@ -92,19 +112,10 @@ def mock_thinq_api(mock_thinq_mqtt_client: AsyncMock) -> Generator[AsyncMock]:
|
||||
|
||||
@pytest.fixture
|
||||
def mock_thinq_mqtt_client() -> Generator[AsyncMock]:
|
||||
"""Mock a thinq api."""
|
||||
"""Mock a thinq mqtt client."""
|
||||
with patch(
|
||||
"homeassistant.components.lg_thinq.mqtt.ThinQMQTTClient", autospec=True
|
||||
) as mock_api:
|
||||
yield mock_api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_invalid_thinq_api(mock_thinq_api: AsyncMock) -> AsyncMock:
|
||||
"""Mock an invalid thinq api."""
|
||||
mock_thinq_api.async_get_device_list = AsyncMock(
|
||||
side_effect=ThinQAPIException(
|
||||
code="1309", message="Not allowed api call", headers=None
|
||||
)
|
||||
)
|
||||
return mock_thinq_api
|
||||
"homeassistant.components.lg_thinq.mqtt.ThinQMQTTClient",
|
||||
autospec=True,
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
@ -15,7 +15,7 @@ from tests.common import MockConfigEntry
|
||||
|
||||
async def test_config_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_thinq_api: AsyncMock,
|
||||
mock_config_thinq_api: AsyncMock,
|
||||
mock_uuid: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
@ -37,11 +37,12 @@ async def test_config_flow(
|
||||
CONF_CONNECT_CLIENT_ID: MOCK_CONNECT_CLIENT_ID,
|
||||
}
|
||||
|
||||
mock_thinq_api.async_get_device_list.assert_called_once()
|
||||
mock_config_thinq_api.async_get_device_list.assert_called_once()
|
||||
|
||||
|
||||
async def test_config_flow_invalid_pat(
|
||||
hass: HomeAssistant, mock_invalid_thinq_api: AsyncMock
|
||||
hass: HomeAssistant,
|
||||
mock_invalid_thinq_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that an thinq flow should be aborted with an invalid PAT."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -55,7 +56,9 @@ async def test_config_flow_invalid_pat(
|
||||
|
||||
|
||||
async def test_config_flow_already_configured(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_thinq_api: AsyncMock
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_config_thinq_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that thinq flow should be aborted when already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
@ -1,22 +1,29 @@
|
||||
"""Tests for the LG ThinQ integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_load_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_thinq_api: AsyncMock,
|
||||
mock_thinq_mqtt_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test load and unload entry."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.components.lg_thinq.ThinQMQTT.async_connect",
|
||||
return_value=True,
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@ -24,3 +31,21 @@ async def test_load_unload_entry(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exception", [AttributeError(), TypeError(), ValueError()])
|
||||
async def test_config_not_ready(
|
||||
hass: HomeAssistant,
|
||||
mock_thinq_api: AsyncMock,
|
||||
mock_thinq_mqtt_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test for setup failure exception occurred."""
|
||||
with patch(
|
||||
"homeassistant.components.lg_thinq.ThinQMQTT.async_connect",
|
||||
side_effect=exception,
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
@ -111,7 +111,7 @@ async def test_options_unsupported_model(
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_CHAT_MODEL: "o1-mini",
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -168,7 +168,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: "none",
|
||||
CONF_PROMPT: "bla",
|
||||
},
|
||||
{
|
||||
@ -202,6 +201,18 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "medium",
|
||||
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_PROMPT: "",
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_PROMPT: "",
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
@ -209,7 +220,12 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_PROMPT: "",
|
||||
},
|
||||
{
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_PROMPT: "",
|
||||
},
|
||||
),
|
||||
@ -338,7 +354,7 @@ async def test_options_web_search_unsupported_model(
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_CHAT_MODEL: "o1-pro",
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_WEB_SEARCH: True,
|
||||
},
|
||||
)
|
||||
|
291
tests/components/renault/fixtures/vehicle_multi.json
Normal file
291
tests/components/renault/fixtures/vehicle_multi.json
Normal file
@ -0,0 +1,291 @@
|
||||
{
|
||||
"accountId": "account-id-2",
|
||||
"country": "IT",
|
||||
"vehicleLinks": [
|
||||
{
|
||||
"brand": "RENAULT",
|
||||
"vin": "VF1AAAAA555777999",
|
||||
"status": "ACTIVE",
|
||||
"linkType": "OWNER",
|
||||
"garageBrand": "RENAULT",
|
||||
"annualMileage": 16000,
|
||||
"mileage": 26464,
|
||||
"startDate": "2017-08-07",
|
||||
"createdDate": "2019-05-23T21:38:16.409008Z",
|
||||
"lastModifiedDate": "2020-11-17T08:41:40.497400Z",
|
||||
"ownershipStartDate": "2017-08-01",
|
||||
"cancellationReason": {},
|
||||
"connectedDriver": {
|
||||
"role": "MAIN_DRIVER",
|
||||
"createdDate": "2019-06-17T09:49:06.880627Z",
|
||||
"lastModifiedDate": "2019-06-17T09:49:06.880627Z"
|
||||
},
|
||||
"vehicleDetails": {
|
||||
"vin": "VF1AAAAA555777999",
|
||||
"registrationDate": "2017-08-01",
|
||||
"firstRegistrationDate": "2017-08-01",
|
||||
"engineType": "5AQ",
|
||||
"engineRatio": "601",
|
||||
"modelSCR": "ZOE",
|
||||
"deliveryCountry": {
|
||||
"code": "FR",
|
||||
"label": "FRANCE"
|
||||
},
|
||||
"family": {
|
||||
"code": "X10",
|
||||
"label": "FAMILLE X10",
|
||||
"group": "007"
|
||||
},
|
||||
"tcu": {
|
||||
"code": "TCU0G2",
|
||||
"label": "TCU VER 0 GEN 2",
|
||||
"group": "E70"
|
||||
},
|
||||
"navigationAssistanceLevel": {
|
||||
"code": "NAV3G5",
|
||||
"label": "LEVEL 3 TYPE 5 NAVIGATION",
|
||||
"group": "408"
|
||||
},
|
||||
"battery": {
|
||||
"code": "BT4AR1",
|
||||
"label": "BATTERIE BT4AR1",
|
||||
"group": "968"
|
||||
},
|
||||
"radioType": {
|
||||
"code": "RAD37A",
|
||||
"label": "RADIO 37A",
|
||||
"group": "425"
|
||||
},
|
||||
"registrationCountry": {
|
||||
"code": "FR"
|
||||
},
|
||||
"brand": {
|
||||
"label": "RENAULT"
|
||||
},
|
||||
"model": {
|
||||
"code": "X101VE",
|
||||
"label": "ZOE",
|
||||
"group": "971"
|
||||
},
|
||||
"gearbox": {
|
||||
"code": "BVEL",
|
||||
"label": "BOITE A VARIATEUR ELECTRIQUE",
|
||||
"group": "427"
|
||||
},
|
||||
"version": {
|
||||
"code": "INT MB 10R"
|
||||
},
|
||||
"energy": {
|
||||
"code": "ELEC",
|
||||
"label": "ELECTRIQUE",
|
||||
"group": "019"
|
||||
},
|
||||
"registrationNumber": "REG-NUMBER",
|
||||
"vcd": "SYTINC/SKTPOU/SAND41/FDIU1/SSESM/MAPSUP/SSCALL/SAND88/SAND90/SQKDRO/SDIFPA/FACBA2/PRLEX1/SSRCAR/CABDO2/TCU0G2/SWALBO/EVTEC1/STANDA/X10/B10/EA2/MB/ELEC/DG/TEMP/TR4X2/RV/ABS/CAREG/LAC/VT003/CPE/RET03/SPROJA/RALU16/CEAVRH/AIRBA1/SERIE/DRA/DRAP08/HARM02/ATAR/TERQG/SFBANA/KM/DPRPN/AVREPL/SSDECA/ASRESP/RDAR02/ALEVA/CACBL2/SOP02C/CTHAB2/TRNOR/LVAVIP/LVAREL/SASURV/KTGREP/SGSCHA/APL03/ALOUCC/CMAR3P/NAV3G5/RAD37A/BVEL/AUTAUG/RNORM/ISOFIX/EQPEUR/HRGM01/SDPCLV/TLFRAN/SPRODI/SAN613/SSAPEX/GENEV1/ELC1/SANCML/PE2012/PHAS1/SAN913/045KWH/BT4AR1/VEC153/X101VE/NBT017/5AQ",
|
||||
"assets": [
|
||||
{
|
||||
"assetType": "PICTURE",
|
||||
"renditions": [
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_LARGE",
|
||||
"url": "https://3dv2.renault.com/ImageFromBookmark?configuration=SKTPOU%2FPRLEX1%2FSTANDA%2FB10%2FEA2%2FDG%2FVT003%2FRET03%2FRALU16%2FDRAP08%2FHARM02%2FTERQG%2FRDAR02%2FALEVA%2FSOP02C%2FTRNOR%2FLVAVIP%2FLVAREL%2FNAV3G5%2FRAD37A%2FSDPCLV%2FTLFRAN%2FGENEV1%2FSAN913%2FBT4AR1%2FNBT017&databaseId=1d514feb-93a6-4b45-8785-e11d2a6f1864&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE"
|
||||
},
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_SMALL",
|
||||
"url": "https://3dv2.renault.com/ImageFromBookmark?configuration=SKTPOU%2FPRLEX1%2FSTANDA%2FB10%2FEA2%2FDG%2FVT003%2FRET03%2FRALU16%2FDRAP08%2FHARM02%2FTERQG%2FRDAR02%2FALEVA%2FSOP02C%2FTRNOR%2FLVAVIP%2FLVAREL%2FNAV3G5%2FRAD37A%2FSDPCLV%2FTLFRAN%2FGENEV1%2FSAN913%2FBT4AR1%2FNBT017&databaseId=1d514feb-93a6-4b45-8785-e11d2a6f1864&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "PDF",
|
||||
"assetRole": "GUIDE",
|
||||
"title": "PDF Guide",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "https://cdn.group.renault.com/ren/gb/myr/assets/x101ve/manual.pdf.asset.pdf/1558704861676.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "URL",
|
||||
"assetRole": "GUIDE",
|
||||
"title": "e-guide",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "http://gb.e-guide.renault.com/eng/Zoe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"title": "10 Fundamentals about getting the best out of your electric vehicle",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "39r6QEKcOM4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"title": "Automatic Climate Control",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "Va2FnZFo_GE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "URL",
|
||||
"assetRole": "CAR",
|
||||
"title": "More videos",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=wfpCMkK1rKI"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"title": "Charging the battery",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "RaEad8DjUJs"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"title": "Charging the battery at a station with a flap",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "zJfd7fJWtr0"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"yearsOfMaintenance": 12,
|
||||
"connectivityTechnology": "RLINK1",
|
||||
"easyConnectStore": false,
|
||||
"electrical": true,
|
||||
"rlinkStore": false,
|
||||
"deliveryDate": "2017-08-11",
|
||||
"retrievedFromDhs": false,
|
||||
"engineEnergyType": "ELEC",
|
||||
"radioCode": "1234"
|
||||
}
|
||||
},
|
||||
{
|
||||
"brand": "RENAULT",
|
||||
"vin": "VF1AAAAA555777123",
|
||||
"status": "ACTIVE",
|
||||
"linkType": "USER",
|
||||
"garageBrand": "RENAULT",
|
||||
"mileage": 346,
|
||||
"startDate": "2020-06-12",
|
||||
"createdDate": "2020-06-12T15:02:00.555432Z",
|
||||
"lastModifiedDate": "2020-06-15T06:21:43.762467Z",
|
||||
"cancellationReason": {},
|
||||
"connectedDriver": {
|
||||
"role": "MAIN_DRIVER",
|
||||
"createdDate": "2020-06-15T06:20:39.107794Z",
|
||||
"lastModifiedDate": "2020-06-15T06:20:39.107794Z"
|
||||
},
|
||||
"vehicleDetails": {
|
||||
"vin": "VF1AAAAA555777123",
|
||||
"engineType": "H5H",
|
||||
"engineRatio": "470",
|
||||
"modelSCR": "CP1",
|
||||
"deliveryCountry": {
|
||||
"code": "BE",
|
||||
"label": "BELGIQUE"
|
||||
},
|
||||
"family": {
|
||||
"code": "XJB",
|
||||
"label": "FAMILLE B+X OVER",
|
||||
"group": "007"
|
||||
},
|
||||
"tcu": {
|
||||
"code": "AIVCT",
|
||||
"label": "AVEC BOITIER CONNECT AIVC",
|
||||
"group": "E70"
|
||||
},
|
||||
"navigationAssistanceLevel": {
|
||||
"code": "",
|
||||
"label": "",
|
||||
"group": ""
|
||||
},
|
||||
"battery": {
|
||||
"code": "SANBAT",
|
||||
"label": "SANS BATTERIE",
|
||||
"group": "968"
|
||||
},
|
||||
"radioType": {
|
||||
"code": "NA406",
|
||||
"label": "A-IVIMINDL, 2BO + 2BI + 2T, MICRO-DOUBLE, FM1/DAB+FM2",
|
||||
"group": "425"
|
||||
},
|
||||
"registrationCountry": {
|
||||
"code": "BE"
|
||||
},
|
||||
"brand": {
|
||||
"label": "RENAULT"
|
||||
},
|
||||
"model": {
|
||||
"code": "XJB1SU",
|
||||
"label": "CAPTUR II",
|
||||
"group": "971"
|
||||
},
|
||||
"gearbox": {
|
||||
"code": "BVA7",
|
||||
"label": "BOITE DE VITESSE AUTOMATIQUE 7 RAPPORTS",
|
||||
"group": "427"
|
||||
},
|
||||
"version": {
|
||||
"code": "ITAMFHA 6TH"
|
||||
},
|
||||
"energy": {
|
||||
"code": "ESS",
|
||||
"label": "ESSENCE",
|
||||
"group": "019"
|
||||
},
|
||||
"registrationNumber": "REG-NUMBER",
|
||||
"vcd": "ADR00/DLIGM2/PGPRT2/FEUAR3/CDVIT1/SKTPOU/SKTPGR/SSCCPC/SSPREM/FDIU2/MAPSTD/RCALL/MET04/DANGMO/ECOMOD/SSRCAR/AIVCT/AVGSI/TPRPE/TSGNE/2TON/ITPK7/MLEXP1/SPERTA/SSPERG/SPERTP/VOLCHA/SREACT/AVOSP1/SWALBO/DWGE01/AVC1A/1234Y/AEBS07/PRAHL/AVCAM/STANDA/XJB/HJB/EA3/MF/ESS/DG/TEMP/TR4X2/AFURGE/RVDIST/ABS/SBARTO/CA02/TOPAN/PBNCH/LAC/VSTLAR/CPE/RET04/2RVLG/RALU17/CEAVRH/AIRBA2/SERIE/DRA/DRAP05/HARM01/ATAR03/SGAV02/SGAR02/BIXPE/BANAL/KM/TPRM3/AVREPL/SSDECA/SFIRBA/ABLAVI/ESPHSA/FPAS2/ALEVA/SCACBA/SOP03C/SSADPC/STHPLG/SKTGRV/VLCUIR/RETIN2/TRSEV1/REPNTC/LVAVIP/LVAREI/SASURV/KTGREP/SGACHA/BEL01/APL03/FSTPO/ALOUC5/CMAR3P/FIPOU2/NA406/BVA7/ECLHB4/RDIF10/PNSTRD/ISOFIX/ENPH01/HRGM01/SANFLT/CSRGAC/SANACF/SDPCLV/TLRP00/SPRODI/SAN613/AVFAP/AIRBDE/CHC03/E06T/SAN806/SSPTLP/SANCML/SSFLEX/SDRQAR/SEXTIN/M2019/PHAS1/SPRTQT/SAN913/STHABT/SSTYAD/HYB01/SSCABA/SANBAT/VEC012/XJB1SU/SSNBT/H5H",
|
||||
"assets": [
|
||||
{
|
||||
"assetType": "PICTURE",
|
||||
"renditions": [
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_LARGE",
|
||||
"url": "https: //3dv2.renault.com/ImageFromBookmark?configuration=ADR00%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FCDVIT1%2FSSCCPC%2FRCALL%2FMET04%2FDANGMO%2FSSRCAR%2FAVGSI%2FITPK7%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLCHA%2FSREACT%2FDWGE01%2FAVCAM%2FHJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRVDIST%2FSBARTO%2FCA02%2FTOPAN%2FPBNCH%2FVSTLAR%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIXPE%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETIN2%2FREPNTC%2FLVAVIP%2FLVAREI%2FSGACHA%2FALOUC5%2FNA406%2FBVA7%2FECLHB4%2FRDIF10%2FCSRGAC%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB01%2FH5H&databaseId=3e814da7-766d-4039-ac69-f001a1f738c8&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE"
|
||||
},
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_SMALL",
|
||||
"url": "https: //3dv2.renault.com/ImageFromBookmark?configuration=ADR00%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FCDVIT1%2FSSCCPC%2FRCALL%2FMET04%2FDANGMO%2FSSRCAR%2FAVGSI%2FITPK7%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLCHA%2FSREACT%2FDWGE01%2FAVCAM%2FHJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRVDIST%2FSBARTO%2FCA02%2FTOPAN%2FPBNCH%2FVSTLAR%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIXPE%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETIN2%2FREPNTC%2FLVAVIP%2FLVAREI%2FSGACHA%2FALOUC5%2FNA406%2FBVA7%2FECLHB4%2FRDIF10%2FCSRGAC%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB01%2FH5H&databaseId=3e814da7-766d-4039-ac69-f001a1f738c8&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"yearsOfMaintenance": 12,
|
||||
"connectivityTechnology": "NONE",
|
||||
"easyConnectStore": false,
|
||||
"electrical": false,
|
||||
"rlinkStore": false,
|
||||
"deliveryDate": "2020-06-17",
|
||||
"retrievedFromDhs": false,
|
||||
"engineEnergyType": "OTHER",
|
||||
"radioCode": "1234"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -2,11 +2,15 @@
|
||||
|
||||
from collections.abc import Generator
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from renault_api.kamereon.exceptions import QuotaLimitException
|
||||
from renault_api.kamereon.exceptions import (
|
||||
AccessDeniedException,
|
||||
NotSupportedException,
|
||||
QuotaLimitException,
|
||||
)
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -241,3 +245,89 @@ async def test_sensor_throttling_after_init(
|
||||
assert not hass.states.get(entity_id).attributes.get(ATTR_ASSUMED_STATE)
|
||||
assert "Renault API throttled" not in caplog.text
|
||||
assert "Renault hub currently throttled: scan skipped" not in caplog.text
|
||||
|
||||
|
||||
# scan interval in seconds = (3600 * num_calls) / MAX_CALLS_PER_HOURS
|
||||
# MAX_CALLS_PER_HOURS being a constant, for now 60 calls per hour
|
||||
# num_calls = num_coordinator_car_0 + num_coordinator_car_1 + ... + num_coordinator_car_n
|
||||
@pytest.mark.parametrize(
|
||||
("vehicle_type", "vehicle_count", "scan_interval"),
|
||||
[
|
||||
("zoe_40", 1, 300), # 5 coordinators => 5 minutes interval
|
||||
("captur_fuel", 1, 240), # 4 coordinators => 4 minutes interval
|
||||
("multi", 2, 540), # 9 coordinators => 9 minutes interval
|
||||
],
|
||||
indirect=["vehicle_type"],
|
||||
)
|
||||
async def test_dynamic_scan_interval(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
vehicle_count: int,
|
||||
scan_interval: int,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
fixtures_with_data: dict[str, AsyncMock],
|
||||
) -> None:
|
||||
"""Test scan interval."""
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert fixtures_with_data["cockpit"].call_count == vehicle_count
|
||||
|
||||
# 2 seconds before the expected scan interval > not called
|
||||
freezer.tick(datetime.timedelta(seconds=scan_interval - 2))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert fixtures_with_data["cockpit"].call_count == vehicle_count
|
||||
|
||||
# 2 seconds after the expected scan interval > called
|
||||
freezer.tick(datetime.timedelta(seconds=4))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert fixtures_with_data["cockpit"].call_count == vehicle_count * 2
|
||||
|
||||
|
||||
# scan interval in seconds = (3600 * num_calls) / MAX_CALLS_PER_HOURS
|
||||
# MAX_CALLS_PER_HOURS being a constant, for now 60 calls per hour
|
||||
# num_calls = num_coordinator_car_0 + num_coordinator_car_1 + ... + num_coordinator_car_n
|
||||
@pytest.mark.parametrize(
|
||||
("vehicle_type", "vehicle_count", "scan_interval"),
|
||||
[
|
||||
("zoe_40", 1, 240), # (5-1) coordinators => 4 minutes interval
|
||||
("captur_fuel", 1, 180), # (4-1) coordinators => 3 minutes interval
|
||||
("multi", 2, 420), # (9-2) coordinators => 7 minutes interval
|
||||
],
|
||||
indirect=["vehicle_type"],
|
||||
)
|
||||
async def test_dynamic_scan_interval_failed_coordinator(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
vehicle_count: int,
|
||||
scan_interval: int,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
fixtures_with_data: dict[str, AsyncMock],
|
||||
) -> None:
|
||||
"""Test scan interval."""
|
||||
fixtures_with_data["battery_status"].side_effect = NotSupportedException(
|
||||
"err.tech.501",
|
||||
"This feature is not technically supported by this gateway",
|
||||
)
|
||||
fixtures_with_data["lock_status"].side_effect = AccessDeniedException(
|
||||
"err.func.403",
|
||||
"Access is denied for this resource",
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert fixtures_with_data["cockpit"].call_count == vehicle_count
|
||||
|
||||
# 2 seconds before the expected scan interval > not called
|
||||
freezer.tick(datetime.timedelta(seconds=scan_interval - 2))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert fixtures_with_data["cockpit"].call_count == vehicle_count
|
||||
|
||||
# 2 seconds after the expected scan interval > called
|
||||
freezer.tick(datetime.timedelta(seconds=4))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert fixtures_with_data["cockpit"].call_count == vehicle_count * 2
|
||||
|
@ -107,7 +107,9 @@ def mock_smartthings() -> Generator[AsyncMock]:
|
||||
"centralite",
|
||||
"da_ref_normal_000001",
|
||||
"da_ref_normal_01011",
|
||||
"da_ref_normal_01001",
|
||||
"vd_network_audio_002s",
|
||||
"vd_network_audio_003s",
|
||||
"vd_sensor_light_2023",
|
||||
"iphone",
|
||||
"da_sac_ehs_000001_sub",
|
||||
|
@ -0,0 +1,929 @@
|
||||
{
|
||||
"components": {
|
||||
"pantry-01": {
|
||||
"samsungce.foodDefrost": {
|
||||
"supportedOptions": {
|
||||
"value": null
|
||||
},
|
||||
"foodType": {
|
||||
"value": null
|
||||
},
|
||||
"weight": {
|
||||
"value": null
|
||||
},
|
||||
"operationTime": {
|
||||
"value": null
|
||||
},
|
||||
"remainingTime": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.fridgePantryInfo": {
|
||||
"name": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": ["samsungce.meatAging", "samsungce.foodDefrost"],
|
||||
"timestamp": "2022-02-07T10:54:05.580Z"
|
||||
}
|
||||
},
|
||||
"samsungce.meatAging": {
|
||||
"zoneInfo": {
|
||||
"value": null
|
||||
},
|
||||
"supportedMeatTypes": {
|
||||
"value": null
|
||||
},
|
||||
"supportedAgingMethods": {
|
||||
"value": null
|
||||
},
|
||||
"status": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.fridgePantryMode": {
|
||||
"mode": {
|
||||
"value": null
|
||||
},
|
||||
"supportedModes": {
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"icemaker": {
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": [],
|
||||
"timestamp": "2022-02-07T10:54:05.580Z"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"switch": {
|
||||
"value": "on",
|
||||
"timestamp": "2025-02-07T12:01:52.528Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scale-10": {
|
||||
"samsungce.connectionState": {
|
||||
"connectionState": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": [],
|
||||
"timestamp": "2022-02-07T10:54:05.580Z"
|
||||
}
|
||||
},
|
||||
"samsungce.weightMeasurement": {
|
||||
"weight": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.scaleSettings": {
|
||||
"enabled": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.weightMeasurementCalibration": {}
|
||||
},
|
||||
"scale-11": {
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": [],
|
||||
"timestamp": "2022-02-07T10:54:05.580Z"
|
||||
}
|
||||
},
|
||||
"samsungce.weightMeasurement": {
|
||||
"weight": {
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"camera-01": {
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": ["switch"],
|
||||
"timestamp": "2023-12-17T11:19:18.845Z"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"switch": {
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"cooler": {
|
||||
"contactSensor": {
|
||||
"contact": {
|
||||
"value": "closed",
|
||||
"timestamp": "2025-02-09T00:23:41.655Z"
|
||||
}
|
||||
},
|
||||
"samsungce.unavailableCapabilities": {
|
||||
"unavailableCommands": {
|
||||
"value": [],
|
||||
"timestamp": "2024-11-06T12:35:50.411Z"
|
||||
}
|
||||
},
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": [],
|
||||
"timestamp": "2024-06-17T06:16:33.918Z"
|
||||
}
|
||||
},
|
||||
"temperatureMeasurement": {
|
||||
"temperatureRange": {
|
||||
"value": null
|
||||
},
|
||||
"temperature": {
|
||||
"value": 37,
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
}
|
||||
},
|
||||
"custom.thermostatSetpointControl": {
|
||||
"minimumSetpoint": {
|
||||
"value": 34,
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
},
|
||||
"maximumSetpoint": {
|
||||
"value": 44,
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
}
|
||||
},
|
||||
"thermostatCoolingSetpoint": {
|
||||
"coolingSetpointRange": {
|
||||
"value": {
|
||||
"minimum": 34,
|
||||
"maximum": 44,
|
||||
"step": 1
|
||||
},
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
},
|
||||
"coolingSetpoint": {
|
||||
"value": 37,
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"freezer": {
|
||||
"contactSensor": {
|
||||
"contact": {
|
||||
"value": "closed",
|
||||
"timestamp": "2025-02-09T00:00:44.267Z"
|
||||
}
|
||||
},
|
||||
"samsungce.unavailableCapabilities": {
|
||||
"unavailableCommands": {
|
||||
"value": [],
|
||||
"timestamp": "2024-11-06T12:35:50.411Z"
|
||||
}
|
||||
},
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": ["samsungce.freezerConvertMode"],
|
||||
"timestamp": "2024-11-06T09:00:29.743Z"
|
||||
}
|
||||
},
|
||||
"temperatureMeasurement": {
|
||||
"temperatureRange": {
|
||||
"value": null
|
||||
},
|
||||
"temperature": {
|
||||
"value": 0,
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
}
|
||||
},
|
||||
"custom.thermostatSetpointControl": {
|
||||
"minimumSetpoint": {
|
||||
"value": -8,
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
},
|
||||
"maximumSetpoint": {
|
||||
"value": 5,
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
}
|
||||
},
|
||||
"samsungce.freezerConvertMode": {
|
||||
"supportedFreezerConvertModes": {
|
||||
"value": [],
|
||||
"timestamp": "2025-02-01T19:39:00.448Z"
|
||||
},
|
||||
"freezerConvertMode": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"thermostatCoolingSetpoint": {
|
||||
"coolingSetpointRange": {
|
||||
"value": {
|
||||
"minimum": -8,
|
||||
"maximum": 5,
|
||||
"step": 1
|
||||
},
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
},
|
||||
"coolingSetpoint": {
|
||||
"value": 0,
|
||||
"unit": "F",
|
||||
"timestamp": "2025-02-01T19:39:00.493Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"contactSensor": {
|
||||
"contact": {
|
||||
"value": "closed",
|
||||
"timestamp": "2025-02-09T00:23:41.655Z"
|
||||
}
|
||||
},
|
||||
"samsungce.viewInside": {
|
||||
"supportedFocusAreas": {
|
||||
"value": ["mainShelves"],
|
||||
"timestamp": "2025-02-01T19:39:00.946Z"
|
||||
},
|
||||
"contents": {
|
||||
"value": [
|
||||
{
|
||||
"fileId": "d3e1f875-f8b3-a031-737b-366eaa227773",
|
||||
"mimeType": "image/jpeg",
|
||||
"expiredTime": "2025-01-20T16:17:04Z",
|
||||
"focusArea": "mainShelves"
|
||||
},
|
||||
{
|
||||
"fileId": "9fccb6b4-e71f-6c7f-9935-f6082bb6ccfe",
|
||||
"mimeType": "image/jpeg",
|
||||
"expiredTime": "2025-01-20T16:17:04Z",
|
||||
"focusArea": "mainShelves"
|
||||
},
|
||||
{
|
||||
"fileId": "20b57a4d-b7fc-17fc-3a03-0fb84fb4efab",
|
||||
"mimeType": "image/jpeg",
|
||||
"expiredTime": "2025-01-20T16:17:05Z",
|
||||
"focusArea": "mainShelves"
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-01-20T16:07:05.423Z"
|
||||
},
|
||||
"lastUpdatedTime": {
|
||||
"value": "2025-02-07T12:01:52Z",
|
||||
"timestamp": "2025-02-07T12:01:52.585Z"
|
||||
}
|
||||
},
|
||||
"samsungce.fridgeFoodList": {
|
||||
"outOfSyncChanges": {
|
||||
"value": null
|
||||
},
|
||||
"refreshResult": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.deviceIdentification": {
|
||||
"micomAssayCode": {
|
||||
"value": null
|
||||
},
|
||||
"modelName": {
|
||||
"value": null
|
||||
},
|
||||
"serialNumber": {
|
||||
"value": null
|
||||
},
|
||||
"serialNumberExtra": {
|
||||
"value": null
|
||||
},
|
||||
"modelClassificationCode": {
|
||||
"value": null
|
||||
},
|
||||
"description": {
|
||||
"value": null
|
||||
},
|
||||
"releaseYear": {
|
||||
"value": 19,
|
||||
"timestamp": "2024-11-06T09:00:29.743Z"
|
||||
},
|
||||
"binaryId": {
|
||||
"value": "24K_REF_LCD_FHUB9.0",
|
||||
"timestamp": "2025-02-07T12:01:53.067Z"
|
||||
}
|
||||
},
|
||||
"samsungce.quickControl": {
|
||||
"version": {
|
||||
"value": "1.0",
|
||||
"timestamp": "2025-02-01T19:39:01.848Z"
|
||||
}
|
||||
},
|
||||
"custom.fridgeMode": {
|
||||
"fridgeModeValue": {
|
||||
"value": null
|
||||
},
|
||||
"fridgeMode": {
|
||||
"value": null
|
||||
},
|
||||
"supportedFridgeModes": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"ocf": {
|
||||
"st": {
|
||||
"value": "2024-11-08T11:56:59Z",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"mndt": {
|
||||
"value": "",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"mnfv": {
|
||||
"value": "20240616.213423",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"mnhw": {
|
||||
"value": "",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"di": {
|
||||
"value": "7d3feb98-8a36-4351-c362-5e21ad3a78dd",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"mnsl": {
|
||||
"value": "",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"dmv": {
|
||||
"value": "res.1.1.0,sh.1.1.0",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"n": {
|
||||
"value": "Family Hub",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"mnmo": {
|
||||
"value": "24K_REF_LCD_FHUB9.0|00113141|0002034e051324200103000000000000",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"vid": {
|
||||
"value": "DA-REF-NORMAL-01001",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"mnmn": {
|
||||
"value": "Samsung Electronics",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"mnml": {
|
||||
"value": "",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"mnpv": {
|
||||
"value": "7.0",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"mnos": {
|
||||
"value": "Tizen",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"pi": {
|
||||
"value": "7d3feb98-8a36-4351-c362-5e21ad3a78dd",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
},
|
||||
"icv": {
|
||||
"value": "core.1.1.0",
|
||||
"timestamp": "2025-01-02T12:37:43.756Z"
|
||||
}
|
||||
},
|
||||
"samsungce.fridgeVacationMode": {
|
||||
"vacationMode": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": [
|
||||
"thermostatCoolingSetpoint",
|
||||
"temperatureMeasurement",
|
||||
"custom.fridgeMode",
|
||||
"custom.deviceReportStateConfiguration",
|
||||
"samsungce.fridgeFoodList",
|
||||
"samsungce.runestoneHomeContext",
|
||||
"demandResponseLoadControl",
|
||||
"samsungce.fridgeVacationMode",
|
||||
"samsungce.sabbathMode"
|
||||
],
|
||||
"timestamp": "2025-02-08T23:57:45.739Z"
|
||||
}
|
||||
},
|
||||
"samsungce.driverVersion": {
|
||||
"versionNumber": {
|
||||
"value": 24090102,
|
||||
"timestamp": "2024-11-06T09:00:29.743Z"
|
||||
}
|
||||
},
|
||||
"sec.diagnosticsInformation": {
|
||||
"logType": {
|
||||
"value": ["errCode", "dump"],
|
||||
"timestamp": "2025-02-01T19:39:00.523Z"
|
||||
},
|
||||
"endpoint": {
|
||||
"value": "SSM",
|
||||
"timestamp": "2025-02-01T19:39:00.523Z"
|
||||
},
|
||||
"minVersion": {
|
||||
"value": "1.0",
|
||||
"timestamp": "2025-02-01T19:39:00.523Z"
|
||||
},
|
||||
"signinPermission": {
|
||||
"value": null
|
||||
},
|
||||
"setupId": {
|
||||
"value": "500",
|
||||
"timestamp": "2025-02-01T19:39:00.523Z"
|
||||
},
|
||||
"protocolType": {
|
||||
"value": "wifi_https",
|
||||
"timestamp": "2025-02-01T19:39:00.523Z"
|
||||
},
|
||||
"tsId": {
|
||||
"value": null
|
||||
},
|
||||
"mnId": {
|
||||
"value": "0AJT",
|
||||
"timestamp": "2025-02-01T19:39:00.523Z"
|
||||
},
|
||||
"dumpType": {
|
||||
"value": "file",
|
||||
"timestamp": "2025-02-01T19:39:00.523Z"
|
||||
}
|
||||
},
|
||||
"temperatureMeasurement": {
|
||||
"temperatureRange": {
|
||||
"value": null
|
||||
},
|
||||
"temperature": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"custom.deviceReportStateConfiguration": {
|
||||
"reportStateRealtimePeriod": {
|
||||
"value": null
|
||||
},
|
||||
"reportStateRealtime": {
|
||||
"value": {
|
||||
"state": "disabled"
|
||||
},
|
||||
"timestamp": "2025-02-01T19:39:00.345Z"
|
||||
},
|
||||
"reportStatePeriod": {
|
||||
"value": "enabled",
|
||||
"timestamp": "2025-02-01T19:39:00.345Z"
|
||||
}
|
||||
},
|
||||
"thermostatCoolingSetpoint": {
|
||||
"coolingSetpointRange": {
|
||||
"value": null
|
||||
},
|
||||
"coolingSetpoint": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"custom.disabledComponents": {
|
||||
"disabledComponents": {
|
||||
"value": [
|
||||
"icemaker-02",
|
||||
"icemaker-03",
|
||||
"pantry-01",
|
||||
"camera-01",
|
||||
"scale-10",
|
||||
"scale-11"
|
||||
],
|
||||
"timestamp": "2025-02-07T12:01:52.638Z"
|
||||
}
|
||||
},
|
||||
"demandResponseLoadControl": {
|
||||
"drlcStatus": {
|
||||
"value": {
|
||||
"drlcType": 1,
|
||||
"drlcLevel": 0,
|
||||
"duration": 0,
|
||||
"override": false
|
||||
},
|
||||
"timestamp": "2025-02-01T19:38:59.899Z"
|
||||
}
|
||||
},
|
||||
"samsungce.sabbathMode": {
|
||||
"supportedActions": {
|
||||
"value": null
|
||||
},
|
||||
"status": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"powerConsumptionReport": {
|
||||
"powerConsumption": {
|
||||
"value": {
|
||||
"energy": 4381422,
|
||||
"deltaEnergy": 27,
|
||||
"power": 144,
|
||||
"powerEnergy": 27.01890500307083,
|
||||
"persistedEnergy": 0,
|
||||
"energySaved": 0,
|
||||
"start": "2025-02-09T00:13:39Z",
|
||||
"end": "2025-02-09T00:25:23Z"
|
||||
},
|
||||
"timestamp": "2025-02-09T00:25:23.843Z"
|
||||
}
|
||||
},
|
||||
"refresh": {},
|
||||
"samsungce.runestoneHomeContext": {
|
||||
"supportedContexts": {
|
||||
"value": [
|
||||
{
|
||||
"context": "HOME_IN",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "ASLEEP",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "AWAKE",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "COOKING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_COOKING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "EATING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_EATING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "DOING_LAUNDRY",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_DOING_LAUNDRY",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "CLEANING_HOUSE",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_CLEANING_HOUSE",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "MUSIC_LISTENING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_MUSIC_LISTENING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "AIR_CONDITIONING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_AIR_CONDITIONING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "WASHING_DISHES",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_WASHING_DISHES",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "CARING_CLOTHING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_CARING_CLOTHING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "WATCHING_TV",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_WATCHING_TV",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "BEFORE_BEDTIME",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "BEFORE_COOKING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "BEFORE_HOME_OUT",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "ORDERING_DELIVERY_FOOD",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_ORDERING_DELIVERY_FOOD",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "ONLINE_GROCERY_SHOPPING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
},
|
||||
{
|
||||
"context": "FINISH_ONLINE_GROCERY_SHOPPING",
|
||||
"place": "HOME",
|
||||
"startTime": "99:99",
|
||||
"endTime": "99:99"
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-02-01T19:39:02.150Z"
|
||||
}
|
||||
},
|
||||
"execute": {
|
||||
"data": {
|
||||
"value": {
|
||||
"payload": {
|
||||
"rt": ["x.com.samsung.da.fridge"],
|
||||
"if": ["oic.if.a"],
|
||||
"x.com.samsung.da.rapidFridge": "Off",
|
||||
"x.com.samsung.da.rapidFreezing": "Off"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"href": "/refrigeration/vs/0"
|
||||
},
|
||||
"timestamp": "2024-03-26T09:06:17.169Z"
|
||||
}
|
||||
},
|
||||
"sec.wifiConfiguration": {
|
||||
"autoReconnection": {
|
||||
"value": true,
|
||||
"timestamp": "2025-02-01T19:39:01.951Z"
|
||||
},
|
||||
"minVersion": {
|
||||
"value": "1.0",
|
||||
"timestamp": "2025-02-01T19:39:01.951Z"
|
||||
},
|
||||
"supportedWiFiFreq": {
|
||||
"value": ["2.4G", "5G"],
|
||||
"timestamp": "2025-02-01T19:39:01.951Z"
|
||||
},
|
||||
"supportedAuthType": {
|
||||
"value": ["OPEN", "WEP", "WPA-PSK", "WPA2-PSK"],
|
||||
"timestamp": "2025-02-01T19:39:01.951Z"
|
||||
},
|
||||
"protocolType": {
|
||||
"value": ["helper_hotspot"],
|
||||
"timestamp": "2025-02-01T19:39:01.951Z"
|
||||
}
|
||||
},
|
||||
"refrigeration": {
|
||||
"defrost": {
|
||||
"value": "off",
|
||||
"timestamp": "2025-02-01T19:38:59.276Z"
|
||||
},
|
||||
"rapidCooling": {
|
||||
"value": "off",
|
||||
"timestamp": "2025-02-01T19:39:00.497Z"
|
||||
},
|
||||
"rapidFreezing": {
|
||||
"value": "off",
|
||||
"timestamp": "2025-02-01T19:39:00.497Z"
|
||||
}
|
||||
},
|
||||
"samsungce.powerCool": {
|
||||
"activated": {
|
||||
"value": false,
|
||||
"timestamp": "2025-02-01T19:39:00.497Z"
|
||||
}
|
||||
},
|
||||
"custom.energyType": {
|
||||
"energyType": {
|
||||
"value": "2.0",
|
||||
"timestamp": "2022-02-07T10:54:05.580Z"
|
||||
},
|
||||
"energySavingSupport": {
|
||||
"value": false,
|
||||
"timestamp": "2022-02-07T10:57:35.490Z"
|
||||
},
|
||||
"drMaxDuration": {
|
||||
"value": 1440,
|
||||
"unit": "min",
|
||||
"timestamp": "2022-02-07T11:50:40.228Z"
|
||||
},
|
||||
"energySavingLevel": {
|
||||
"value": null
|
||||
},
|
||||
"energySavingInfo": {
|
||||
"value": null
|
||||
},
|
||||
"supportedEnergySavingLevels": {
|
||||
"value": null
|
||||
},
|
||||
"energySavingOperation": {
|
||||
"value": null
|
||||
},
|
||||
"notificationTemplateID": {
|
||||
"value": null
|
||||
},
|
||||
"energySavingOperationSupport": {
|
||||
"value": false,
|
||||
"timestamp": "2022-02-07T11:50:40.228Z"
|
||||
}
|
||||
},
|
||||
"samsungce.softwareUpdate": {
|
||||
"targetModule": {
|
||||
"value": {},
|
||||
"timestamp": "2025-02-01T19:39:00.200Z"
|
||||
},
|
||||
"otnDUID": {
|
||||
"value": "2DCEZFTFQZPMO",
|
||||
"timestamp": "2025-02-01T19:39:00.523Z"
|
||||
},
|
||||
"lastUpdatedDate": {
|
||||
"value": null
|
||||
},
|
||||
"availableModules": {
|
||||
"value": [],
|
||||
"timestamp": "2025-02-01T19:39:00.523Z"
|
||||
},
|
||||
"newVersionAvailable": {
|
||||
"value": false,
|
||||
"timestamp": "2025-02-01T19:39:00.200Z"
|
||||
},
|
||||
"operatingState": {
|
||||
"value": null
|
||||
},
|
||||
"progress": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.powerFreeze": {
|
||||
"activated": {
|
||||
"value": false,
|
||||
"timestamp": "2025-02-01T19:39:00.497Z"
|
||||
}
|
||||
},
|
||||
"custom.waterFilter": {
|
||||
"waterFilterUsageStep": {
|
||||
"value": 1,
|
||||
"timestamp": "2025-02-01T19:38:59.973Z"
|
||||
},
|
||||
"waterFilterResetType": {
|
||||
"value": ["replaceable"],
|
||||
"timestamp": "2025-02-01T19:38:59.973Z"
|
||||
},
|
||||
"waterFilterCapacity": {
|
||||
"value": null
|
||||
},
|
||||
"waterFilterLastResetDate": {
|
||||
"value": null
|
||||
},
|
||||
"waterFilterUsage": {
|
||||
"value": 52,
|
||||
"timestamp": "2025-02-08T05:06:45.769Z"
|
||||
},
|
||||
"waterFilterStatus": {
|
||||
"value": "normal",
|
||||
"timestamp": "2025-02-01T19:38:59.973Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cvroom": {
|
||||
"custom.fridgeMode": {
|
||||
"fridgeModeValue": {
|
||||
"value": null
|
||||
},
|
||||
"fridgeMode": {
|
||||
"value": "CV_FDR_DELI",
|
||||
"timestamp": "2025-02-01T19:39:00.448Z"
|
||||
},
|
||||
"supportedFridgeModes": {
|
||||
"value": [
|
||||
"CV_FDR_WINE",
|
||||
"CV_FDR_DELI",
|
||||
"CV_FDR_BEVERAGE",
|
||||
"CV_FDR_MEAT"
|
||||
],
|
||||
"timestamp": "2025-02-01T19:39:00.448Z"
|
||||
}
|
||||
},
|
||||
"contactSensor": {
|
||||
"contact": {
|
||||
"value": "closed",
|
||||
"timestamp": "2025-02-08T23:22:04.631Z"
|
||||
}
|
||||
},
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": [],
|
||||
"timestamp": "2021-07-27T01:19:43.145Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"icemaker-02": {
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": [],
|
||||
"timestamp": "2022-07-28T18:47:07.039Z"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"switch": {
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"icemaker-03": {
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": [],
|
||||
"timestamp": "2023-12-15T01:05:09.803Z"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"switch": {
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
{
|
||||
"components": {
|
||||
"main": {
|
||||
"samsungvd.soundFrom": {
|
||||
"mode": {
|
||||
"value": 29,
|
||||
"timestamp": "2025-04-05T13:51:47.865Z"
|
||||
},
|
||||
"detailName": {
|
||||
"value": "None",
|
||||
"timestamp": "2025-04-05T13:51:50.230Z"
|
||||
}
|
||||
},
|
||||
"audioVolume": {
|
||||
"volume": {
|
||||
"value": 6,
|
||||
"unit": "%",
|
||||
"timestamp": "2025-04-17T11:17:25.272Z"
|
||||
}
|
||||
},
|
||||
"samsungvd.audioGroupInfo": {
|
||||
"role": {
|
||||
"value": null
|
||||
},
|
||||
"channel": {
|
||||
"value": null
|
||||
},
|
||||
"status": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"refresh": {},
|
||||
"audioNotification": {},
|
||||
"execute": {
|
||||
"data": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungvd.audioInputSource": {
|
||||
"supportedInputSources": {
|
||||
"value": ["D.IN", "BT", "WIFI"],
|
||||
"timestamp": "2025-03-18T19:11:54.071Z"
|
||||
},
|
||||
"inputSource": {
|
||||
"value": "D.IN",
|
||||
"timestamp": "2025-04-17T11:18:02.048Z"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"switch": {
|
||||
"value": "off",
|
||||
"timestamp": "2025-04-17T14:42:04.704Z"
|
||||
}
|
||||
},
|
||||
"sec.wifiConfiguration": {
|
||||
"autoReconnection": {
|
||||
"value": true,
|
||||
"timestamp": "2025-03-18T19:11:54.484Z"
|
||||
},
|
||||
"minVersion": {
|
||||
"value": "1.0",
|
||||
"timestamp": "2025-03-18T19:11:54.484Z"
|
||||
},
|
||||
"supportedWiFiFreq": {
|
||||
"value": ["2.4G", "5G"],
|
||||
"timestamp": "2025-03-18T19:11:54.484Z"
|
||||
},
|
||||
"supportedAuthType": {
|
||||
"value": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"EAP",
|
||||
"SAE",
|
||||
"OWE",
|
||||
"FT-PSK"
|
||||
],
|
||||
"timestamp": "2025-03-18T19:11:54.484Z"
|
||||
},
|
||||
"protocolType": {
|
||||
"value": ["ble_ocf"],
|
||||
"timestamp": "2025-03-18T19:11:54.484Z"
|
||||
}
|
||||
},
|
||||
"ocf": {
|
||||
"st": {
|
||||
"value": "1970-01-01T00:00:47Z",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"mndt": {
|
||||
"value": "2024-01-01",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"mnfv": {
|
||||
"value": "SAT-MT8532D24WWC-1016.0",
|
||||
"timestamp": "2025-02-21T16:47:38.134Z"
|
||||
},
|
||||
"mnhw": {
|
||||
"value": "",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"di": {
|
||||
"value": "a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"mnsl": {
|
||||
"value": "",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"dmv": {
|
||||
"value": "res.1.1.0,sh.1.1.0",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"n": {
|
||||
"value": "Soundbar",
|
||||
"timestamp": "2025-02-21T16:47:38.134Z"
|
||||
},
|
||||
"mnmo": {
|
||||
"value": "HW-S60D",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"vid": {
|
||||
"value": "VD-NetworkAudio-003S",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"mnmn": {
|
||||
"value": "Samsung Electronics",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"mnml": {
|
||||
"value": "",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"mnpv": {
|
||||
"value": "8.0",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"mnos": {
|
||||
"value": "Tizen",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"pi": {
|
||||
"value": "a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
},
|
||||
"icv": {
|
||||
"value": "core.1.1.0",
|
||||
"timestamp": "2025-02-21T15:09:52.348Z"
|
||||
}
|
||||
},
|
||||
"samsungvd.supportsFeatures": {
|
||||
"mediaOutputSupported": {
|
||||
"value": null
|
||||
},
|
||||
"imeAdvSupported": {
|
||||
"value": null
|
||||
},
|
||||
"wifiUpdateSupport": {
|
||||
"value": true,
|
||||
"timestamp": "2025-03-18T19:11:53.853Z"
|
||||
},
|
||||
"executableServiceList": {
|
||||
"value": null
|
||||
},
|
||||
"remotelessSupported": {
|
||||
"value": null
|
||||
},
|
||||
"artSupported": {
|
||||
"value": null
|
||||
},
|
||||
"mobileCamSupported": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"sec.diagnosticsInformation": {
|
||||
"logType": {
|
||||
"value": ["errCode", "dump"],
|
||||
"timestamp": "2025-03-18T19:11:54.336Z"
|
||||
},
|
||||
"endpoint": {
|
||||
"value": "PIPER",
|
||||
"timestamp": "2025-03-18T19:11:54.336Z"
|
||||
},
|
||||
"minVersion": {
|
||||
"value": "3.0",
|
||||
"timestamp": "2025-03-18T19:11:54.336Z"
|
||||
},
|
||||
"signinPermission": {
|
||||
"value": null
|
||||
},
|
||||
"setupId": {
|
||||
"value": "301",
|
||||
"timestamp": "2025-03-18T19:11:54.336Z"
|
||||
},
|
||||
"protocolType": {
|
||||
"value": "ble_ocf",
|
||||
"timestamp": "2025-03-18T19:11:54.336Z"
|
||||
},
|
||||
"tsId": {
|
||||
"value": "VD02",
|
||||
"timestamp": "2025-03-18T19:11:54.336Z"
|
||||
},
|
||||
"mnId": {
|
||||
"value": "0AJK",
|
||||
"timestamp": "2025-03-18T19:11:54.336Z"
|
||||
},
|
||||
"dumpType": {
|
||||
"value": "file",
|
||||
"timestamp": "2025-03-18T19:11:54.336Z"
|
||||
}
|
||||
},
|
||||
"audioMute": {
|
||||
"mute": {
|
||||
"value": "muted",
|
||||
"timestamp": "2025-04-17T11:36:04.814Z"
|
||||
}
|
||||
},
|
||||
"samsungvd.thingStatus": {
|
||||
"updatedTime": {
|
||||
"value": 1744900925,
|
||||
"timestamp": "2025-04-17T14:42:04.770Z"
|
||||
},
|
||||
"status": {
|
||||
"value": "Idle",
|
||||
"timestamp": "2025-03-18T19:11:54.101Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,433 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"deviceId": "7d3feb98-8a36-4351-c362-5e21ad3a78dd",
|
||||
"name": "Family Hub",
|
||||
"label": "Refrigerator",
|
||||
"manufacturerName": "Samsung Electronics",
|
||||
"presentationId": "DA-REF-NORMAL-01001",
|
||||
"deviceManufacturerCode": "Samsung Electronics",
|
||||
"locationId": "2487472a-06c4-4bce-8f4c-700c5f8644f8",
|
||||
"ownerId": "b603d7e8-6066-4e10-8102-afa752a63816",
|
||||
"roomId": "acaa060a-7c19-4579-8a4a-5ad891a2f0c1",
|
||||
"deviceTypeName": "Samsung OCF Refrigerator",
|
||||
"components": [
|
||||
{
|
||||
"id": "main",
|
||||
"label": "main",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "contactSensor",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "execute",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "ocf",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "powerConsumptionReport",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "demandResponseLoadControl",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "refresh",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "refrigeration",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "temperatureMeasurement",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "thermostatCoolingSetpoint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.deviceReportStateConfiguration",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.energyType",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.fridgeMode",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledComponents",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.waterFilter",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.fridgeFoodList",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.softwareUpdate",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.deviceIdentification",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.driverVersion",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.fridgeVacationMode",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.powerCool",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.powerFreeze",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.sabbathMode",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.viewInside",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.runestoneHomeContext",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.quickControl",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "sec.diagnosticsInformation",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "sec.wifiConfiguration",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Refrigerator",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "freezer",
|
||||
"label": "freezer",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "contactSensor",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "temperatureMeasurement",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "thermostatCoolingSetpoint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.thermostatSetpointControl",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.freezerConvertMode",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.unavailableCapabilities",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cooler",
|
||||
"label": "cooler",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "contactSensor",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "temperatureMeasurement",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "thermostatCoolingSetpoint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.thermostatSetpointControl",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.unavailableCapabilities",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cvroom",
|
||||
"label": "cvroom",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "contactSensor",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.fridgeMode",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "icemaker",
|
||||
"label": "icemaker",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "switch",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "icemaker-02",
|
||||
"label": "icemaker-02",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "switch",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "icemaker-03",
|
||||
"label": "icemaker-03",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "switch",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "scale-10",
|
||||
"label": "scale-10",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "samsungce.weightMeasurement",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.weightMeasurementCalibration",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.connectionState",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.scaleSettings",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "scale-11",
|
||||
"label": "scale-11",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "samsungce.weightMeasurement",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pantry-01",
|
||||
"label": "pantry-01",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "samsungce.fridgePantryInfo",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.fridgePantryMode",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.meatAging",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.foodDefrost",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "camera-01",
|
||||
"label": "camera-01",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "switch",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Other",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createTime": "2021-07-27T01:19:42.051Z",
|
||||
"profile": {
|
||||
"id": "4c654f1b-8ef4-35b0-920e-c12568554213"
|
||||
},
|
||||
"ocf": {
|
||||
"ocfDeviceType": "oic.d.refrigerator",
|
||||
"name": "Family Hub",
|
||||
"specVersion": "core.1.1.0",
|
||||
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
|
||||
"manufacturerName": "Samsung Electronics",
|
||||
"modelNumber": "24K_REF_LCD_FHUB9.0|00113141|0002034e051324200103000000000000",
|
||||
"platformVersion": "7.0",
|
||||
"platformOS": "Tizen",
|
||||
"hwVersion": "",
|
||||
"firmwareVersion": "20240616.213423",
|
||||
"vendorId": "DA-REF-NORMAL-01001",
|
||||
"vendorResourceClientServerVersion": "4.0.22",
|
||||
"locale": "",
|
||||
"lastSignupTime": "2021-07-27T01:19:40.244392Z",
|
||||
"transferCandidate": false,
|
||||
"additionalAuthCodeRequired": false
|
||||
},
|
||||
"type": "OCF",
|
||||
"restrictionTier": 0,
|
||||
"allowed": [],
|
||||
"executionContext": "CLOUD"
|
||||
}
|
||||
],
|
||||
"_links": {}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"deviceId": "a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6",
|
||||
"name": "Soundbar",
|
||||
"label": "Soundbar",
|
||||
"manufacturerName": "Samsung Electronics",
|
||||
"presentationId": "VD-NetworkAudio-003S",
|
||||
"deviceManufacturerCode": "Samsung Electronics",
|
||||
"locationId": "6bdf6730-8167-488b-8645-d0c5046ff763",
|
||||
"ownerId": "15f0ae72-da51-14e2-65cf-ef59ae867e7f",
|
||||
"roomId": "3b0fe9a8-51d6-49cf-b64a-8a719013c0a7",
|
||||
"deviceTypeName": "Samsung OCF Network Audio Player",
|
||||
"components": [
|
||||
{
|
||||
"id": "main",
|
||||
"label": "main",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "ocf",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "execute",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "refresh",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "switch",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "audioVolume",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "audioMute",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungvd.audioInputSource",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "audioNotification",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungvd.soundFrom",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "sec.diagnosticsInformation",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungvd.thingStatus",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungvd.supportsFeatures",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "sec.wifiConfiguration",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungvd.audioGroupInfo",
|
||||
"version": 1,
|
||||
"ephemeral": true
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "NetworkAudio",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
],
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"createTime": "2025-02-21T14:25:21.843Z",
|
||||
"profile": {
|
||||
"id": "25504ad5-8563-3b07-8770-e52ad29a9c5a"
|
||||
},
|
||||
"ocf": {
|
||||
"ocfDeviceType": "oic.d.networkaudio",
|
||||
"name": "Soundbar",
|
||||
"specVersion": "core.1.1.0",
|
||||
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
|
||||
"manufacturerName": "Samsung Electronics",
|
||||
"modelNumber": "HW-S60D",
|
||||
"platformVersion": "8.0",
|
||||
"platformOS": "Tizen",
|
||||
"hwVersion": "",
|
||||
"firmwareVersion": "SAT-MT8532D24WWC-1016.0",
|
||||
"vendorId": "VD-NetworkAudio-003S",
|
||||
"vendorResourceClientServerVersion": "4.0.26",
|
||||
"lastSignupTime": "2025-03-18T19:11:51.176292902Z",
|
||||
"transferCandidate": false,
|
||||
"additionalAuthCodeRequired": false
|
||||
},
|
||||
"type": "OCF",
|
||||
"restrictionTier": 0,
|
||||
"allowed": null,
|
||||
"executionContext": "CLOUD",
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"_links": {}
|
||||
}
|
@ -761,6 +761,150 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_cooler_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.refrigerator_cooler_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooler door',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cooler_door',
|
||||
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_contactSensor_contact_contact',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_cooler_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'Refrigerator Cooler door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.refrigerator_cooler_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_coolselect_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.refrigerator_coolselect_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'CoolSelect+ door',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cool_select_plus_door',
|
||||
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cvroom_contactSensor_contact_contact',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_coolselect_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'Refrigerator CoolSelect+ door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.refrigerator_coolselect_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_freezer_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.refrigerator_freezer_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Freezer door',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'freezer_door',
|
||||
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_freezer_contactSensor_contact_contact',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_freezer_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'Refrigerator Freezer door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.refrigerator_freezer_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_cooler_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@ -187,3 +187,50 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][button.refrigerator_reset_water_filter-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': None,
|
||||
'entity_id': 'button.refrigerator_reset_water_filter',
|
||||
'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': 'Reset water filter',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'reset_water_filter',
|
||||
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_custom.waterFilter_resetWaterFilter',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][button.refrigerator_reset_water_filter-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Refrigerator Reset water filter',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.refrigerator_reset_water_filter',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
|
@ -629,6 +629,39 @@
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[da_ref_normal_01001]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': 'https://account.smartthings.com',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'smartthings',
|
||||
'7d3feb98-8a36-4351-c362-5e21ad3a78dd',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Samsung Electronics',
|
||||
'model': '24K_REF_LCD_FHUB9.0',
|
||||
'model_id': None,
|
||||
'name': 'Refrigerator',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': '20240616.213423',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[da_ref_normal_01011]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
@ -1652,6 +1685,39 @@
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[vd_network_audio_003s]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': 'https://account.smartthings.com',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': '',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'smartthings',
|
||||
'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Samsung Electronics',
|
||||
'model': 'HW-S60D',
|
||||
'model_id': None,
|
||||
'name': 'Soundbar',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': 'SAT-MT8532D24WWC-1016.0',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[vd_sensor_light_2023]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
|
@ -231,6 +231,56 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[vd_network_audio_003s][media_player.soundbar-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.soundbar',
|
||||
'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': None,
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 1420>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6_main',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[vd_network_audio_003s][media_player.soundbar-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'speaker',
|
||||
'friendly_name': 'Soundbar',
|
||||
'supported_features': <MediaPlayerEntityFeature: 1420>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.soundbar',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[vd_stv_2017_k][media_player.tv_samsung_8_series_49-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@ -4049,6 +4049,283 @@
|
||||
'state': '0.0135559777781698',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.refrigerator_energy',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Energy',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_energy_meter',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Refrigerator Energy',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.refrigerator_energy',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4381.422',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_difference-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.refrigerator_energy_difference',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Energy difference',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'energy_difference',
|
||||
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_difference-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Refrigerator Energy difference',
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.refrigerator_energy_difference',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.027',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_saved-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.refrigerator_energy_saved',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Energy saved',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'energy_saved',
|
||||
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_energySaved_meter',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy_saved-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Refrigerator Energy saved',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.refrigerator_energy_saved',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.refrigerator_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_power_meter',
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Refrigerator Power',
|
||||
'power_consumption_end': '2025-02-09T00:25:23Z',
|
||||
'power_consumption_start': '2025-02-09T00:13:39Z',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.refrigerator_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '144',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power_energy-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.refrigerator_power_energy',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power energy',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'power_energy',
|
||||
'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_powerEnergy_meter',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Refrigerator Power energy',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.refrigerator_power_energy',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0270189050030708',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user