mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Merge branch 'dev' into whirlpool_sensor_door_remove
This commit is contained in:
commit
32308649a0
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -24,11 +24,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.28.16
|
uses: github/codeql-action/init@v3.28.17
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.28.16
|
uses: github/codeql-action/analyze@v3.28.17
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -1796,6 +1796,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/zeversolar/ @kvanzuijlen
|
/tests/components/zeversolar/ @kvanzuijlen
|
||||||
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
||||||
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
||||||
|
/homeassistant/components/zimi/ @markhannon
|
||||||
|
/tests/components/zimi/ @markhannon
|
||||||
/homeassistant/components/zodiac/ @JulienTant
|
/homeassistant/components/zodiac/ @JulienTant
|
||||||
/tests/components/zodiac/ @JulienTant
|
/tests/components/zodiac/ @JulienTant
|
||||||
/homeassistant/components/zone/ @home-assistant/core
|
/homeassistant/components/zone/ @home-assistant/core
|
||||||
|
@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = {
|
|||||||
2: "moderate",
|
2: "moderate",
|
||||||
3: "high",
|
3: "high",
|
||||||
4: "very_high",
|
4: "very_high",
|
||||||
|
5: "extreme",
|
||||||
}
|
}
|
||||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
||||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||||
|
@ -72,6 +72,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "Level",
|
"name": "Level",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "Extreme",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "Moderate",
|
"moderate": "Moderate",
|
||||||
@ -89,6 +90,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
@ -123,6 +125,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
@ -167,6 +170,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
@ -181,6 +185,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
@ -195,6 +200,7 @@
|
|||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
|
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||||
|
@ -30,6 +30,7 @@ class BackupCoordinatorData:
|
|||||||
"""Class to hold backup data."""
|
"""Class to hold backup data."""
|
||||||
|
|
||||||
backup_manager_state: BackupManagerState
|
backup_manager_state: BackupManagerState
|
||||||
|
last_attempted_automatic_backup: datetime | None
|
||||||
last_successful_automatic_backup: datetime | None
|
last_successful_automatic_backup: datetime | None
|
||||||
next_scheduled_automatic_backup: datetime | None
|
next_scheduled_automatic_backup: datetime | None
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
|
|||||||
"""Update backup manager data."""
|
"""Update backup manager data."""
|
||||||
return BackupCoordinatorData(
|
return BackupCoordinatorData(
|
||||||
self.backup_manager.state,
|
self.backup_manager.state,
|
||||||
|
self.backup_manager.config.data.last_attempted_automatic_backup,
|
||||||
self.backup_manager.config.data.last_completed_automatic_backup,
|
self.backup_manager.config.data.last_completed_automatic_backup,
|
||||||
self.backup_manager.config.data.schedule.next_automatic_backup,
|
self.backup_manager.config.data.schedule.next_automatic_backup,
|
||||||
)
|
)
|
||||||
|
@ -46,6 +46,12 @@ BACKUP_MANAGER_DESCRIPTIONS = (
|
|||||||
device_class=SensorDeviceClass.TIMESTAMP,
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
value_fn=lambda data: data.last_successful_automatic_backup,
|
value_fn=lambda data: data.last_successful_automatic_backup,
|
||||||
),
|
),
|
||||||
|
BackupSensorEntityDescription(
|
||||||
|
key="last_attempted_automatic_backup",
|
||||||
|
translation_key="last_attempted_automatic_backup",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
value_fn=lambda data: data.last_attempted_automatic_backup,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +37,9 @@
|
|||||||
"next_scheduled_automatic_backup": {
|
"next_scheduled_automatic_backup": {
|
||||||
"name": "Next scheduled automatic backup"
|
"name": "Next scheduled automatic backup"
|
||||||
},
|
},
|
||||||
|
"last_attempted_automatic_backup": {
|
||||||
|
"name": "Last attempted automatic backup"
|
||||||
|
},
|
||||||
"last_successful_automatic_backup": {
|
"last_successful_automatic_backup": {
|
||||||
"name": "Last successful automatic backup"
|
"name": "Last successful automatic backup"
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Sign-in with Blink account",
|
"title": "Sign in with Blink account",
|
||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
@ -30,7 +30,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"simple_options": {
|
"simple_options": {
|
||||||
"data": {
|
"data": {
|
||||||
"scan_interval": "Scan Interval (seconds)"
|
"scan_interval": "Scan interval (seconds)"
|
||||||
},
|
},
|
||||||
"title": "Blink options",
|
"title": "Blink options",
|
||||||
"description": "Configure Blink integration"
|
"description": "Configure Blink integration"
|
||||||
@ -93,7 +93,7 @@
|
|||||||
},
|
},
|
||||||
"config_entry_id": {
|
"config_entry_id": {
|
||||||
"name": "Integration ID",
|
"name": "Integration ID",
|
||||||
"description": "The Blink Integration ID."
|
"description": "The Blink integration ID."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,9 @@
|
|||||||
"bleak==0.22.3",
|
"bleak==0.22.3",
|
||||||
"bleak-retry-connector==3.9.0",
|
"bleak-retry-connector==3.9.0",
|
||||||
"bluetooth-adapters==0.21.4",
|
"bluetooth-adapters==0.21.4",
|
||||||
"bluetooth-auto-recovery==1.4.5",
|
"bluetooth-auto-recovery==1.5.1",
|
||||||
"bluetooth-data-tools==1.28.1",
|
"bluetooth-data-tools==1.28.1",
|
||||||
"dbus-fast==2.43.0",
|
"dbus-fast==2.43.0",
|
||||||
"habluetooth==3.45.0"
|
"habluetooth==3.48.2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import ClientError, ClientResponseError, ClientTimeout
|
from aiohttp import ClientError, ClientResponseError, ClientTimeout
|
||||||
from bond_async import Bond, BPUPSubscriptions, start_bpup
|
from bond_async import Bond, BPUPSubscriptions, RequestorUUID, start_bpup
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -49,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool
|
|||||||
token=token,
|
token=token,
|
||||||
timeout=ClientTimeout(total=_API_TIMEOUT),
|
timeout=ClientTimeout(total=_API_TIMEOUT),
|
||||||
session=async_get_clientsession(hass),
|
session=async_get_clientsession(hass),
|
||||||
|
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
|
||||||
)
|
)
|
||||||
hub = BondHub(bond, host)
|
hub = BondHub(bond, host)
|
||||||
try:
|
try:
|
||||||
|
@ -8,7 +8,7 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import ClientConnectionError, ClientResponseError
|
from aiohttp import ClientConnectionError, ClientResponseError
|
||||||
from bond_async import Bond
|
from bond_async import Bond, RequestorUUID
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
|
||||||
@ -34,7 +34,12 @@ TOKEN_SCHEMA = vol.Schema({})
|
|||||||
|
|
||||||
async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
|
async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
|
||||||
"""Try to fetch the token from the bond device."""
|
"""Try to fetch the token from the bond device."""
|
||||||
bond = Bond(host, "", session=async_get_clientsession(hass))
|
bond = Bond(
|
||||||
|
host,
|
||||||
|
"",
|
||||||
|
session=async_get_clientsession(hass),
|
||||||
|
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
|
||||||
|
)
|
||||||
response: dict[str, str] = {}
|
response: dict[str, str] = {}
|
||||||
with contextlib.suppress(ClientConnectionError):
|
with contextlib.suppress(ClientConnectionError):
|
||||||
response = await bond.token()
|
response = await bond.token()
|
||||||
@ -45,7 +50,10 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st
|
|||||||
"""Validate the user input allows us to connect."""
|
"""Validate the user input allows us to connect."""
|
||||||
|
|
||||||
bond = Bond(
|
bond = Bond(
|
||||||
data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass)
|
data[CONF_HOST],
|
||||||
|
data[CONF_ACCESS_TOKEN],
|
||||||
|
session=async_get_clientsession(hass),
|
||||||
|
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
hub = BondHub(bond, data[CONF_HOST])
|
hub = BondHub(bond, data[CONF_HOST])
|
||||||
|
@ -10,12 +10,12 @@
|
|||||||
"known_hosts": "Add known host"
|
"known_hosts": "Add known host"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"known_hosts": "Hostnames or IP-addresses of cast devices, use if mDNS discovery is not working"
|
"known_hosts": "Hostnames or IP addresses of cast devices, use if mDNS discovery is not working"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
|
"invalid_known_hosts": "Known hosts must be a comma-separated list of hosts."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["denonavr"],
|
"loggers": ["denonavr"],
|
||||||
"requirements": ["denonavr==1.0.1"],
|
"requirements": ["denonavr==1.1.0"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Denon",
|
"manufacturer": "Denon",
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["aiodns==3.2.0"]
|
"requirements": ["aiodns==3.3.0"]
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
"events": "Comma separated list of events."
|
"events": "Comma-separated list of events."
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"events": "Add a comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
|
"events": "Add a comma-separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,5 +22,5 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["eq3btsmart"],
|
"loggers": ["eq3btsmart"],
|
||||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"]
|
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ from collections.abc import Callable, Iterable
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
from operator import delitem
|
||||||
from typing import TYPE_CHECKING, Any, Final, TypedDict, cast
|
from typing import TYPE_CHECKING, Any, Final, TypedDict, cast
|
||||||
|
|
||||||
from aioesphomeapi import (
|
from aioesphomeapi import (
|
||||||
@ -183,18 +184,7 @@ class RuntimeEntryData:
|
|||||||
"""Register to receive callbacks when static info changes for an EntityInfo type."""
|
"""Register to receive callbacks when static info changes for an EntityInfo type."""
|
||||||
callbacks = self.entity_info_callbacks.setdefault(entity_info_type, [])
|
callbacks = self.entity_info_callbacks.setdefault(entity_info_type, [])
|
||||||
callbacks.append(callback_)
|
callbacks.append(callback_)
|
||||||
return partial(
|
return partial(callbacks.remove, callback_)
|
||||||
self._async_unsubscribe_register_static_info, callbacks, callback_
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_register_static_info(
|
|
||||||
self,
|
|
||||||
callbacks: list[Callable[[list[EntityInfo]], None]],
|
|
||||||
callback_: Callable[[list[EntityInfo]], None],
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to when static info is registered."""
|
|
||||||
callbacks.remove(callback_)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_key_static_info_updated_callback(
|
def async_register_key_static_info_updated_callback(
|
||||||
@ -206,18 +196,7 @@ class RuntimeEntryData:
|
|||||||
callback_key = (type(static_info), static_info.key)
|
callback_key = (type(static_info), static_info.key)
|
||||||
callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, [])
|
callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, [])
|
||||||
callbacks.append(callback_)
|
callbacks.append(callback_)
|
||||||
return partial(
|
return partial(callbacks.remove, callback_)
|
||||||
self._async_unsubscribe_static_key_info_updated, callbacks, callback_
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_static_key_info_updated(
|
|
||||||
self,
|
|
||||||
callbacks: list[Callable[[EntityInfo], None]],
|
|
||||||
callback_: Callable[[EntityInfo], None],
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to when static info is updated ."""
|
|
||||||
callbacks.remove(callback_)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_set_assist_pipeline_state(self, state: bool) -> None:
|
def async_set_assist_pipeline_state(self, state: bool) -> None:
|
||||||
@ -232,14 +211,7 @@ class RuntimeEntryData:
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Subscribe to assist pipeline updates."""
|
"""Subscribe to assist pipeline updates."""
|
||||||
self.assist_pipeline_update_callbacks.append(update_callback)
|
self.assist_pipeline_update_callbacks.append(update_callback)
|
||||||
return partial(self._async_unsubscribe_assist_pipeline_update, update_callback)
|
return partial(self.assist_pipeline_update_callbacks.remove, update_callback)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_assist_pipeline_update(
|
|
||||||
self, update_callback: CALLBACK_TYPE
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to assist pipeline updates."""
|
|
||||||
self.assist_pipeline_update_callbacks.remove(update_callback)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_remove_entities(
|
def async_remove_entities(
|
||||||
@ -337,12 +309,7 @@ class RuntimeEntryData:
|
|||||||
def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
||||||
"""Subscribe to state updates."""
|
"""Subscribe to state updates."""
|
||||||
self.device_update_subscriptions.add(callback_)
|
self.device_update_subscriptions.add(callback_)
|
||||||
return partial(self._async_unsubscribe_device_update, callback_)
|
return partial(self.device_update_subscriptions.remove, callback_)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_device_update(self, callback_: CALLBACK_TYPE) -> None:
|
|
||||||
"""Unsubscribe to device updates."""
|
|
||||||
self.device_update_subscriptions.remove(callback_)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_subscribe_static_info_updated(
|
def async_subscribe_static_info_updated(
|
||||||
@ -350,14 +317,7 @@ class RuntimeEntryData:
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Subscribe to static info updates."""
|
"""Subscribe to static info updates."""
|
||||||
self.static_info_update_subscriptions.add(callback_)
|
self.static_info_update_subscriptions.add(callback_)
|
||||||
return partial(self._async_unsubscribe_static_info_updated, callback_)
|
return partial(self.static_info_update_subscriptions.remove, callback_)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_static_info_updated(
|
|
||||||
self, callback_: Callable[[list[EntityInfo]], None]
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to static info updates."""
|
|
||||||
self.static_info_update_subscriptions.remove(callback_)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_subscribe_state_update(
|
def async_subscribe_state_update(
|
||||||
@ -369,14 +329,7 @@ class RuntimeEntryData:
|
|||||||
"""Subscribe to state updates."""
|
"""Subscribe to state updates."""
|
||||||
subscription_key = (state_type, state_key)
|
subscription_key = (state_type, state_key)
|
||||||
self.state_subscriptions[subscription_key] = entity_callback
|
self.state_subscriptions[subscription_key] = entity_callback
|
||||||
return partial(self._async_unsubscribe_state_update, subscription_key)
|
return partial(delitem, self.state_subscriptions, subscription_key)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_unsubscribe_state_update(
|
|
||||||
self, subscription_key: tuple[type[EntityState], int]
|
|
||||||
) -> None:
|
|
||||||
"""Unsubscribe to state updates."""
|
|
||||||
self.state_subscriptions.pop(subscription_key)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_state(self, state: EntityState) -> None:
|
def async_update_state(self, state: EntityState) -> None:
|
||||||
@ -523,7 +476,7 @@ class RuntimeEntryData:
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Register to receive callbacks when the Assist satellite's configuration is updated."""
|
"""Register to receive callbacks when the Assist satellite's configuration is updated."""
|
||||||
self.assist_satellite_config_update_callbacks.append(callback_)
|
self.assist_satellite_config_update_callbacks.append(callback_)
|
||||||
return lambda: self.assist_satellite_config_update_callbacks.remove(callback_)
|
return partial(self.assist_satellite_config_update_callbacks.remove, callback_)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_assist_satellite_config_updated(
|
def async_assist_satellite_config_updated(
|
||||||
@ -540,7 +493,7 @@ class RuntimeEntryData:
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Register to receive callbacks when the Assist satellite's wake word is set."""
|
"""Register to receive callbacks when the Assist satellite's wake word is set."""
|
||||||
self.assist_satellite_set_wake_word_callbacks.append(callback_)
|
self.assist_satellite_set_wake_word_callbacks.append(callback_)
|
||||||
return lambda: self.assist_satellite_set_wake_word_callbacks.remove(callback_)
|
return partial(self.assist_satellite_set_wake_word_callbacks.remove, callback_)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None:
|
def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None:
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"aioesphomeapi==30.1.0",
|
"aioesphomeapi==30.1.0",
|
||||||
"esphome-dashboard-api==1.3.0",
|
"esphome-dashboard-api==1.3.0",
|
||||||
"bleak-esphome==2.14.0"
|
"bleak-esphome==2.15.1"
|
||||||
],
|
],
|
||||||
"zeroconf": ["_esphomelib._tcp.local."]
|
"zeroconf": ["_esphomelib._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,10 @@
|
|||||||
"message": "Error compiling {configuration}; Try again in ESPHome dashboard for more information."
|
"message": "Error compiling {configuration}; Try again in ESPHome dashboard for more information."
|
||||||
},
|
},
|
||||||
"error_uploading": {
|
"error_uploading": {
|
||||||
"message": "Error during OTA of {configuration}; Try again in ESPHome dashboard for more information."
|
"message": "Error during OTA (Over-The-Air) of {configuration}; Try again in ESPHome dashboard for more information."
|
||||||
|
},
|
||||||
|
"ota_in_progress": {
|
||||||
|
"message": "An OTA (Over-The-Air) update is already in progress for {configuration}."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,21 +125,17 @@ class ESPHomeDashboardUpdateEntity(
|
|||||||
(dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address)
|
(dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
self._install_lock = asyncio.Lock()
|
||||||
|
self._available_future: asyncio.Future[None] | None = None
|
||||||
self._update_attrs()
|
self._update_attrs()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_attrs(self) -> None:
|
def _update_attrs(self) -> None:
|
||||||
"""Update the supported features."""
|
"""Update the supported features."""
|
||||||
# If the device has deep sleep, we can't assume we can install updates
|
|
||||||
# as the ESP will not be connectable (by design).
|
|
||||||
coordinator = self.coordinator
|
coordinator = self.coordinator
|
||||||
device_info = self._device_info
|
device_info = self._device_info
|
||||||
# Install support can change at run time
|
# Install support can change at run time
|
||||||
if (
|
if coordinator.last_update_success and coordinator.supports_update:
|
||||||
coordinator.last_update_success
|
|
||||||
and coordinator.supports_update
|
|
||||||
and not device_info.has_deep_sleep
|
|
||||||
):
|
|
||||||
self._attr_supported_features = UpdateEntityFeature.INSTALL
|
self._attr_supported_features = UpdateEntityFeature.INSTALL
|
||||||
else:
|
else:
|
||||||
self._attr_supported_features = NO_FEATURES
|
self._attr_supported_features = NO_FEATURES
|
||||||
@ -178,6 +174,13 @@ class ESPHomeDashboardUpdateEntity(
|
|||||||
self, static_info: list[EntityInfo] | None = None
|
self, static_info: list[EntityInfo] | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle updated data from the device."""
|
"""Handle updated data from the device."""
|
||||||
|
if (
|
||||||
|
self._entry_data.available
|
||||||
|
and self._available_future
|
||||||
|
and not self._available_future.done()
|
||||||
|
):
|
||||||
|
self._available_future.set_result(None)
|
||||||
|
self._available_future = None
|
||||||
self._update_attrs()
|
self._update_attrs()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@ -192,17 +195,46 @@ class ESPHomeDashboardUpdateEntity(
|
|||||||
entry_data.async_subscribe_device_updated(self._handle_device_update)
|
entry_data.async_subscribe_device_updated(self._handle_device_update)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Handle entity about to be removed from Home Assistant."""
|
||||||
|
if self._available_future and not self._available_future.done():
|
||||||
|
self._available_future.cancel()
|
||||||
|
self._available_future = None
|
||||||
|
|
||||||
|
async def _async_wait_available(self) -> None:
|
||||||
|
"""Wait until the device is available."""
|
||||||
|
# If the device has deep sleep, we need to wait for it to wake up
|
||||||
|
# and connect to the network to be able to install the update.
|
||||||
|
if self._entry_data.available:
|
||||||
|
return
|
||||||
|
self._available_future = self.hass.loop.create_future()
|
||||||
|
try:
|
||||||
|
await self._available_future
|
||||||
|
finally:
|
||||||
|
self._available_future = None
|
||||||
|
|
||||||
async def async_install(
|
async def async_install(
|
||||||
self, version: str | None, backup: bool, **kwargs: Any
|
self, version: str | None, backup: bool, **kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Install an update."""
|
"""Install an update."""
|
||||||
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
|
if self._install_lock.locked():
|
||||||
coordinator = self.coordinator
|
raise HomeAssistantError(
|
||||||
api = coordinator.api
|
translation_domain=DOMAIN,
|
||||||
device = coordinator.data.get(self._device_info.name)
|
translation_key="ota_in_progress",
|
||||||
assert device is not None
|
translation_placeholders={
|
||||||
configuration = device["configuration"]
|
"configuration": self._device_info.name,
|
||||||
try:
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure only one OTA per device at a time
|
||||||
|
async with self._install_lock:
|
||||||
|
# Ensure only one compile at a time for ALL devices
|
||||||
|
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
|
||||||
|
coordinator = self.coordinator
|
||||||
|
api = coordinator.api
|
||||||
|
device = coordinator.data.get(self._device_info.name)
|
||||||
|
assert device is not None
|
||||||
|
configuration = device["configuration"]
|
||||||
if not await api.compile(configuration):
|
if not await api.compile(configuration):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
@ -211,14 +243,25 @@ class ESPHomeDashboardUpdateEntity(
|
|||||||
"configuration": configuration,
|
"configuration": configuration,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if not await api.upload(configuration, "OTA"):
|
|
||||||
raise HomeAssistantError(
|
# If the device uses deep sleep, there's a small chance it goes
|
||||||
translation_domain=DOMAIN,
|
# to sleep right after the dashboard connects but before the OTA
|
||||||
translation_key="error_uploading",
|
# starts. In that case, the update won't go through, so we try
|
||||||
translation_placeholders={
|
# again to catch it on its next wakeup.
|
||||||
"configuration": configuration,
|
attempts = 2 if self._device_info.has_deep_sleep else 1
|
||||||
},
|
try:
|
||||||
)
|
for attempt in range(1, attempts + 1):
|
||||||
|
await self._async_wait_available()
|
||||||
|
if await api.upload(configuration, "OTA"):
|
||||||
|
break
|
||||||
|
if attempt == attempts:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="error_uploading",
|
||||||
|
translation_placeholders={
|
||||||
|
"configuration": configuration,
|
||||||
|
},
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_FEATURE_DEVICE_TRACKING,
|
||||||
|
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||||
DEFAULT_SSL,
|
DEFAULT_SSL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FRITZ_AUTH_EXCEPTIONS,
|
FRITZ_AUTH_EXCEPTIONS,
|
||||||
@ -38,6 +40,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> bool:
|
||||||
"""Set up fritzboxtools from config entry."""
|
"""Set up fritzboxtools from config entry."""
|
||||||
_LOGGER.debug("Setting up FRITZ!Box Tools component")
|
_LOGGER.debug("Setting up FRITZ!Box Tools component")
|
||||||
|
|
||||||
avm_wrapper = AvmWrapper(
|
avm_wrapper = AvmWrapper(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
config_entry=entry,
|
config_entry=entry,
|
||||||
@ -46,6 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo
|
|||||||
username=entry.data[CONF_USERNAME],
|
username=entry.data[CONF_USERNAME],
|
||||||
password=entry.data[CONF_PASSWORD],
|
password=entry.data[CONF_PASSWORD],
|
||||||
use_tls=entry.data.get(CONF_SSL, DEFAULT_SSL),
|
use_tls=entry.data.get(CONF_SSL, DEFAULT_SSL),
|
||||||
|
device_discovery_enabled=entry.options.get(
|
||||||
|
CONF_FEATURE_DEVICE_TRACKING, DEFAULT_CONF_FEATURE_DEVICE_TRACKING
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -62,6 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo
|
|||||||
raise ConfigEntryAuthFailed("Missing UPnP configuration")
|
raise ConfigEntryAuthFailed("Missing UPnP configuration")
|
||||||
|
|
||||||
await avm_wrapper.async_config_entry_first_refresh()
|
await avm_wrapper.async_config_entry_first_refresh()
|
||||||
|
await avm_wrapper.async_trigger_cleanup()
|
||||||
|
|
||||||
entry.runtime_data = avm_wrapper
|
entry.runtime_data = avm_wrapper
|
||||||
|
|
||||||
|
@ -35,7 +35,9 @@ from homeassistant.helpers.service_info.ssdp import (
|
|||||||
from homeassistant.helpers.typing import VolDictType
|
from homeassistant.helpers.typing import VolDictType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_FEATURE_DEVICE_TRACKING,
|
||||||
CONF_OLD_DISCOVERY,
|
CONF_OLD_DISCOVERY,
|
||||||
|
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||||
DEFAULT_CONF_OLD_DISCOVERY,
|
DEFAULT_CONF_OLD_DISCOVERY,
|
||||||
DEFAULT_HOST,
|
DEFAULT_HOST,
|
||||||
DEFAULT_HTTP_PORT,
|
DEFAULT_HTTP_PORT,
|
||||||
@ -72,7 +74,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Initialize FRITZ!Box Tools flow."""
|
"""Initialize FRITZ!Box Tools flow."""
|
||||||
self._name: str = ""
|
self._name: str = ""
|
||||||
self._password: str = ""
|
self._password: str = ""
|
||||||
self._use_tls: bool = False
|
self._use_tls: bool = DEFAULT_SSL
|
||||||
|
self._feature_device_discovery: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING
|
||||||
self._port: int | None = None
|
self._port: int | None = None
|
||||||
self._username: str = ""
|
self._username: str = ""
|
||||||
self._model: str = ""
|
self._model: str = ""
|
||||||
@ -141,6 +144,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
options={
|
options={
|
||||||
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
|
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
|
||||||
CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY,
|
CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY,
|
||||||
|
CONF_FEATURE_DEVICE_TRACKING: self._feature_device_discovery,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -204,6 +208,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
self._username = user_input[CONF_USERNAME]
|
self._username = user_input[CONF_USERNAME]
|
||||||
self._password = user_input[CONF_PASSWORD]
|
self._password = user_input[CONF_PASSWORD]
|
||||||
self._use_tls = user_input[CONF_SSL]
|
self._use_tls = user_input[CONF_SSL]
|
||||||
|
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
|
||||||
self._port = self._determine_port(user_input)
|
self._port = self._determine_port(user_input)
|
||||||
|
|
||||||
error = await self.async_fritz_tools_init()
|
error = await self.async_fritz_tools_init()
|
||||||
@ -234,6 +239,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
vol.Required(CONF_USERNAME): str,
|
vol.Required(CONF_USERNAME): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_PASSWORD): str,
|
||||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
|
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
|
||||||
|
vol.Required(
|
||||||
|
CONF_FEATURE_DEVICE_TRACKING,
|
||||||
|
default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||||
|
): bool,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
errors=errors or {},
|
errors=errors or {},
|
||||||
@ -250,6 +259,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
vol.Required(CONF_USERNAME): str,
|
vol.Required(CONF_USERNAME): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_PASSWORD): str,
|
||||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
|
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
|
||||||
|
vol.Required(
|
||||||
|
CONF_FEATURE_DEVICE_TRACKING,
|
||||||
|
default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||||
|
): bool,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
description_placeholders={"name": self._name},
|
description_placeholders={"name": self._name},
|
||||||
@ -405,7 +418,7 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
|
|||||||
"""Handle options flow."""
|
"""Handle options flow."""
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(data=user_input)
|
||||||
|
|
||||||
options = self.config_entry.options
|
options = self.config_entry.options
|
||||||
data_schema = vol.Schema(
|
data_schema = vol.Schema(
|
||||||
@ -420,6 +433,13 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
|
|||||||
CONF_OLD_DISCOVERY,
|
CONF_OLD_DISCOVERY,
|
||||||
default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
|
default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
|
||||||
): bool,
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_FEATURE_DEVICE_TRACKING,
|
||||||
|
default=options.get(
|
||||||
|
CONF_FEATURE_DEVICE_TRACKING,
|
||||||
|
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||||
|
),
|
||||||
|
): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return self.async_show_form(step_id="init", data_schema=data_schema)
|
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||||
|
@ -40,6 +40,9 @@ PLATFORMS = [
|
|||||||
CONF_OLD_DISCOVERY = "old_discovery"
|
CONF_OLD_DISCOVERY = "old_discovery"
|
||||||
DEFAULT_CONF_OLD_DISCOVERY = False
|
DEFAULT_CONF_OLD_DISCOVERY = False
|
||||||
|
|
||||||
|
CONF_FEATURE_DEVICE_TRACKING = "feature_device_tracking"
|
||||||
|
DEFAULT_CONF_FEATURE_DEVICE_TRACKING = True
|
||||||
|
|
||||||
DSL_CONNECTION: Literal["dsl"] = "dsl"
|
DSL_CONNECTION: Literal["dsl"] = "dsl"
|
||||||
|
|
||||||
DEFAULT_DEVICE_NAME = "Unknown device"
|
DEFAULT_DEVICE_NAME = "Unknown device"
|
||||||
|
@ -39,6 +39,7 @@ from homeassistant.util.hass_dict import HassKey
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_OLD_DISCOVERY,
|
CONF_OLD_DISCOVERY,
|
||||||
|
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||||
DEFAULT_CONF_OLD_DISCOVERY,
|
DEFAULT_CONF_OLD_DISCOVERY,
|
||||||
DEFAULT_HOST,
|
DEFAULT_HOST,
|
||||||
DEFAULT_SSL,
|
DEFAULT_SSL,
|
||||||
@ -175,6 +176,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
username: str = DEFAULT_USERNAME,
|
username: str = DEFAULT_USERNAME,
|
||||||
host: str = DEFAULT_HOST,
|
host: str = DEFAULT_HOST,
|
||||||
use_tls: bool = DEFAULT_SSL,
|
use_tls: bool = DEFAULT_SSL,
|
||||||
|
device_discovery_enabled: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize FritzboxTools class."""
|
"""Initialize FritzboxTools class."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@ -202,6 +204,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
self.port = port
|
self.port = port
|
||||||
self.username = username
|
self.username = username
|
||||||
self.use_tls = use_tls
|
self.use_tls = use_tls
|
||||||
|
self.device_discovery_enabled = device_discovery_enabled
|
||||||
self.has_call_deflections: bool = False
|
self.has_call_deflections: bool = False
|
||||||
self._model: str | None = None
|
self._model: str | None = None
|
||||||
self._current_firmware: str | None = None
|
self._current_firmware: str | None = None
|
||||||
@ -332,10 +335,15 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
"entity_states": {},
|
"entity_states": {},
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
await self.async_scan_devices()
|
await self.async_update_device_info()
|
||||||
|
|
||||||
|
if self.device_discovery_enabled:
|
||||||
|
await self.async_scan_devices()
|
||||||
|
|
||||||
entity_data["entity_states"] = await self.hass.async_add_executor_job(
|
entity_data["entity_states"] = await self.hass.async_add_executor_job(
|
||||||
self._entity_states_update
|
self._entity_states_update
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.has_call_deflections:
|
if self.has_call_deflections:
|
||||||
entity_data[
|
entity_data[
|
||||||
"call_deflections"
|
"call_deflections"
|
||||||
@ -521,7 +529,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def manage_device_info(
|
def manage_device_info(
|
||||||
self, dev_info: Device, dev_mac: str, consider_home: bool
|
self, dev_info: Device, dev_mac: str, consider_home: float
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Update device lists and return if device is new."""
|
"""Update device lists and return if device is new."""
|
||||||
_LOGGER.debug("Client dev_info: %s", dev_info)
|
_LOGGER.debug("Client dev_info: %s", dev_info)
|
||||||
@ -551,12 +559,8 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
if new_device:
|
if new_device:
|
||||||
async_dispatcher_send(self.hass, self.signal_device_new)
|
async_dispatcher_send(self.hass, self.signal_device_new)
|
||||||
|
|
||||||
async def async_scan_devices(self, now: datetime | None = None) -> None:
|
async def async_update_device_info(self, now: datetime | None = None) -> None:
|
||||||
"""Scan for new devices and return a list of found device ids."""
|
"""Update own device information."""
|
||||||
|
|
||||||
if self.hass.is_stopping:
|
|
||||||
_ha_is_stopping("scan devices")
|
|
||||||
return
|
|
||||||
|
|
||||||
_LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host)
|
_LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host)
|
||||||
(
|
(
|
||||||
@ -565,6 +569,13 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
self._release_url,
|
self._release_url,
|
||||||
) = await self._async_update_device_info()
|
) = await self._async_update_device_info()
|
||||||
|
|
||||||
|
async def async_scan_devices(self, now: datetime | None = None) -> None:
|
||||||
|
"""Scan for new network devices."""
|
||||||
|
|
||||||
|
if self.hass.is_stopping:
|
||||||
|
_ha_is_stopping("scan devices")
|
||||||
|
return
|
||||||
|
|
||||||
_LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host)
|
_LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host)
|
||||||
_default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds()
|
_default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds()
|
||||||
if self._options:
|
if self._options:
|
||||||
@ -683,7 +694,10 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
|
|
||||||
async def async_trigger_cleanup(self) -> None:
|
async def async_trigger_cleanup(self) -> None:
|
||||||
"""Trigger device trackers cleanup."""
|
"""Trigger device trackers cleanup."""
|
||||||
device_hosts = await self._async_update_hosts_info()
|
_LOGGER.debug("Device tracker cleanup triggered")
|
||||||
|
device_hosts = {self.mac: Device(True, "", "", "", "", None)}
|
||||||
|
if self.device_discovery_enabled:
|
||||||
|
device_hosts = await self._async_update_hosts_info()
|
||||||
entity_reg: er.EntityRegistry = er.async_get(self.hass)
|
entity_reg: er.EntityRegistry = er.async_get(self.hass)
|
||||||
config_entry = self.config_entry
|
config_entry = self.config_entry
|
||||||
|
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
"data_description_port": "Leave empty to use the default port.",
|
"data_description_port": "Leave empty to use the default port.",
|
||||||
"data_description_username": "Username for the FRITZ!Box.",
|
"data_description_username": "Username for the FRITZ!Box.",
|
||||||
"data_description_password": "Password for the FRITZ!Box.",
|
"data_description_password": "Password for the FRITZ!Box.",
|
||||||
"data_description_ssl": "Use SSL to connect to the FRITZ!Box."
|
"data_description_ssl": "Use SSL to connect to the FRITZ!Box.",
|
||||||
|
"data_description_feature_device_tracking": "Enable or disable the network device tracking feature.",
|
||||||
|
"data_feature_device_tracking": "Enable network device tracking"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"flow_title": "{name}",
|
"flow_title": "{name}",
|
||||||
@ -15,12 +17,14 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"ssl": "[%key:common::config_flow::data::ssl%]"
|
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||||
|
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"username": "[%key:component::fritz::common::data_description_username%]",
|
"username": "[%key:component::fritz::common::data_description_username%]",
|
||||||
"password": "[%key:component::fritz::common::data_description_password%]",
|
"password": "[%key:component::fritz::common::data_description_password%]",
|
||||||
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
|
"ssl": "[%key:component::fritz::common::data_description_ssl%]",
|
||||||
|
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
@ -57,14 +61,16 @@
|
|||||||
"port": "[%key:common::config_flow::data::port%]",
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"ssl": "[%key:common::config_flow::data::ssl%]"
|
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||||
|
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "[%key:component::fritz::common::data_description_host%]",
|
"host": "[%key:component::fritz::common::data_description_host%]",
|
||||||
"port": "[%key:component::fritz::common::data_description_port%]",
|
"port": "[%key:component::fritz::common::data_description_port%]",
|
||||||
"username": "[%key:component::fritz::common::data_description_username%]",
|
"username": "[%key:component::fritz::common::data_description_username%]",
|
||||||
"password": "[%key:component::fritz::common::data_description_password%]",
|
"password": "[%key:component::fritz::common::data_description_password%]",
|
||||||
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
|
"ssl": "[%key:component::fritz::common::data_description_ssl%]",
|
||||||
|
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -89,11 +95,13 @@
|
|||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
"consider_home": "Seconds to consider a device at 'home'",
|
"consider_home": "Seconds to consider a device at 'home'",
|
||||||
"old_discovery": "Enable old discovery method"
|
"old_discovery": "Enable old discovery method",
|
||||||
|
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"consider_home": "Time in seconds to consider a device at home. Default is 180 seconds.",
|
"consider_home": "Time in seconds to consider a device at home. Default is 180 seconds.",
|
||||||
"old_discovery": "Enable old discovery method. This is needed for some scenarios."
|
"old_discovery": "Enable old discovery method. This is needed for some scenarios.",
|
||||||
|
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,6 +144,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
|
self.check_active_or_lock_mode()
|
||||||
if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF:
|
if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF:
|
||||||
await self.async_set_hkr_state("off")
|
await self.async_set_hkr_state("off")
|
||||||
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||||
@ -168,11 +169,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""Set new operation mode."""
|
"""Set new operation mode."""
|
||||||
if self.data.holiday_active or self.data.summer_active:
|
self.check_active_or_lock_mode()
|
||||||
raise HomeAssistantError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="change_hvac_while_active_mode",
|
|
||||||
)
|
|
||||||
if self.hvac_mode is hvac_mode:
|
if self.hvac_mode is hvac_mode:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"%s is already in requested hvac mode %s", self.name, hvac_mode
|
"%s is already in requested hvac mode %s", self.name, hvac_mode
|
||||||
@ -204,11 +201,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set preset mode."""
|
"""Set preset mode."""
|
||||||
if self.data.holiday_active or self.data.summer_active:
|
self.check_active_or_lock_mode()
|
||||||
raise HomeAssistantError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="change_preset_while_active_mode",
|
|
||||||
)
|
|
||||||
await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode])
|
await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -230,3 +223,17 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
|||||||
attrs[ATTR_STATE_WINDOW_OPEN] = self.data.window_open
|
attrs[ATTR_STATE_WINDOW_OPEN] = self.data.window_open
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
def check_active_or_lock_mode(self) -> None:
|
||||||
|
"""Check if in summer/vacation mode or lock enabled."""
|
||||||
|
if self.data.holiday_active or self.data.summer_active:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="change_settings_while_active_mode",
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.data.lock:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="change_settings_while_lock_enabled",
|
||||||
|
)
|
||||||
|
@ -88,11 +88,11 @@
|
|||||||
"manual_switching_disabled": {
|
"manual_switching_disabled": {
|
||||||
"message": "Can't toggle switch while manual switching is disabled for the device."
|
"message": "Can't toggle switch while manual switching is disabled for the device."
|
||||||
},
|
},
|
||||||
"change_preset_while_active_mode": {
|
"change_settings_while_lock_enabled": {
|
||||||
"message": "Can't change preset while holiday or summer mode is active on the device."
|
"message": "Can't change settings while manual access for telephone, app, or user interface is disabled on the device"
|
||||||
},
|
},
|
||||||
"change_hvac_while_active_mode": {
|
"change_settings_while_active_mode": {
|
||||||
"message": "Can't change HVAC mode while holiday or summer mode is active on the device."
|
"message": "Can't change settings while holiday or summer mode is active on the device."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,9 +39,9 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Configure Prefixes",
|
"title": "Configure prefixes",
|
||||||
"data": {
|
"data": {
|
||||||
"prefixes": "Prefixes (comma separated list)"
|
"prefixes": "Prefixes (comma-separated list)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -140,16 +140,16 @@
|
|||||||
"ac_module_temperature_sensor_faulty_l3": "AC module temperature sensor faulty (L3)",
|
"ac_module_temperature_sensor_faulty_l3": "AC module temperature sensor faulty (L3)",
|
||||||
"dc_module_temperature_sensor_faulty": "DC module temperature sensor faulty",
|
"dc_module_temperature_sensor_faulty": "DC module temperature sensor faulty",
|
||||||
"internal_processor_status": "Warning about the internal processor status. See status code for more information",
|
"internal_processor_status": "Warning about the internal processor status. See status code for more information",
|
||||||
"eeprom_reinitialised": "EEPROM has been re-initialised",
|
"eeprom_reinitialised": "EEPROM has been re-initialized",
|
||||||
"initialisation_error_usb_flash_drive_not_supported": "Initialisation error – USB flash drive is not supported",
|
"initialisation_error_usb_flash_drive_not_supported": "Initialization error – USB flash drive is not supported",
|
||||||
"initialisation_error_usb_stick_over_current": "Initialisation error – Overcurrent on USB stick",
|
"initialisation_error_usb_stick_over_current": "Initialization error – Overcurrent on USB stick",
|
||||||
"no_usb_flash_drive_connected": "No USB flash drive connected",
|
"no_usb_flash_drive_connected": "No USB flash drive connected",
|
||||||
"update_file_not_recognised_or_missing": "Update file not recognised or not present",
|
"update_file_not_recognised_or_missing": "Update file not recognized or not present",
|
||||||
"update_file_does_not_match_device": "Update file does not match the device, update file too old",
|
"update_file_does_not_match_device": "Update file does not match the device, update file too old",
|
||||||
"write_or_read_error_occurred": "Write or read error occurred",
|
"write_or_read_error_occurred": "Write or read error occurred",
|
||||||
"file_could_not_be_opened": "File could not be opened",
|
"file_could_not_be_opened": "File could not be opened",
|
||||||
"log_file_cannot_be_saved": "Log file cannot be saved (e.g. USB flash drive is write protected or full)",
|
"log_file_cannot_be_saved": "Log file cannot be saved (e.g. USB flash drive is write protected or full)",
|
||||||
"initialisation_error_file_system_error_on_usb": "Initialisation error in file system on USB flash drive",
|
"initialisation_error_file_system_error_on_usb": "Initialization error in file system on USB flash drive",
|
||||||
"error_during_logging_data_recording": "Error during recording of logging data",
|
"error_during_logging_data_recording": "Error during recording of logging data",
|
||||||
"error_during_update_process": "Error occurred during update process",
|
"error_during_update_process": "Error occurred during update process",
|
||||||
"update_file_corrupt": "Update file corrupt",
|
"update_file_corrupt": "Update file corrupt",
|
||||||
|
@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250430.2"]
|
"requirements": ["home-assistant-frontend==20250502.0"]
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["googleapiclient"],
|
"loggers": ["googleapiclient"],
|
||||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"]
|
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.0"]
|
||||||
}
|
}
|
||||||
|
@ -56,8 +56,8 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "HEOS Options",
|
"title": "HEOS options",
|
||||||
"description": "You can sign-in to your HEOS Account to access favorites, streaming services, and other features. Clearing the credentials will sign-out of your account.",
|
"description": "You can sign in to your HEOS Account to access favorites, streaming services, and other features. Clearing the credentials will sign out of your account.",
|
||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
@ -102,7 +102,7 @@
|
|||||||
},
|
},
|
||||||
"move_queue_item": {
|
"move_queue_item": {
|
||||||
"name": "Move queue item",
|
"name": "Move queue item",
|
||||||
"description": "Move one or more items within the play queue.",
|
"description": "Moves one or more items within the play queue.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"queue_ids": {
|
"queue_ids": {
|
||||||
"name": "Queue IDs",
|
"name": "Queue IDs",
|
||||||
|
@ -4,6 +4,20 @@
|
|||||||
"codeowners": ["@DavidMStraub", "@Diegorro98", "@MartinHjelmare"],
|
"codeowners": ["@DavidMStraub", "@Diegorro98", "@MartinHjelmare"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["application_credentials", "repairs"],
|
"dependencies": ["application_credentials", "repairs"],
|
||||||
|
"dhcp": [
|
||||||
|
{
|
||||||
|
"hostname": "balay-*",
|
||||||
|
"macaddress": "C8D778*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hostname": "(bosch|siemens)-*",
|
||||||
|
"macaddress": "68A40E*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hostname": "siemens-*",
|
||||||
|
"macaddress": "38B4D3*"
|
||||||
|
}
|
||||||
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aiohomeconnect"],
|
"loggers": ["aiohomeconnect"],
|
||||||
|
@ -79,7 +79,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
|
|||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Call when entity is added to hass."""
|
"""Call when entity is added to hass."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
|
if self.bsh_key is SettingKey.BSH_COMMON_ALARM_CLOCK:
|
||||||
automations = automations_with_entity(self.hass, self.entity_id)
|
automations = automations_with_entity(self.hass, self.entity_id)
|
||||||
scripts = scripts_with_entity(self.hass, self.entity_id)
|
scripts = scripts_with_entity(self.hass, self.entity_id)
|
||||||
items = automations + scripts
|
items = automations + scripts
|
||||||
@ -123,7 +123,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
|
|||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Call when entity will be removed from hass."""
|
"""Call when entity will be removed from hass."""
|
||||||
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
|
if self.bsh_key is SettingKey.BSH_COMMON_ALARM_CLOCK:
|
||||||
async_delete_issue(
|
async_delete_issue(
|
||||||
self.hass,
|
self.hass,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -9,10 +9,10 @@ from typing import Any
|
|||||||
|
|
||||||
from homematicip.async_home import AsyncHome
|
from homematicip.async_home import AsyncHome
|
||||||
from homematicip.auth import Auth
|
from homematicip.auth import Auth
|
||||||
from homematicip.base.base_connection import HmipConnectionError
|
|
||||||
from homematicip.base.enums import EventType
|
from homematicip.base.enums import EventType
|
||||||
from homematicip.connection.connection_context import ConnectionContextBuilder
|
from homematicip.connection.connection_context import ConnectionContextBuilder
|
||||||
from homematicip.connection.rest_connection import RestConnection
|
from homematicip.connection.rest_connection import RestConnection
|
||||||
|
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||||
|
|
||||||
import homeassistant
|
import homeassistant
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["homematicip"],
|
"loggers": ["homematicip"],
|
||||||
"requirements": ["homematicip==2.0.1"]
|
"requirements": ["homematicip==2.0.1.1"]
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ from huawei_lte_api.exceptions import (
|
|||||||
from requests.exceptions import Timeout
|
from requests.exceptions import Timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_HW_VERSION,
|
ATTR_HW_VERSION,
|
||||||
@ -90,36 +89,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
NOTIFY_SCHEMA = vol.Any(
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
None,
|
|
||||||
vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
|
||||||
vol.Optional(CONF_RECIPIENT): vol.Any(
|
|
||||||
None, vol.All(cv.ensure_list, [cv.string])
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
DOMAIN: vol.All(
|
|
||||||
cv.ensure_list,
|
|
||||||
[
|
|
||||||
vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_URL): cv.url,
|
|
||||||
vol.Optional(CONF_USERNAME): cv.string,
|
|
||||||
vol.Optional(CONF_PASSWORD): cv.string,
|
|
||||||
vol.Optional(NOTIFY_DOMAIN): NOTIFY_SCHEMA,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
},
|
|
||||||
extra=vol.ALLOW_EXTRA,
|
|
||||||
)
|
|
||||||
|
|
||||||
SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.url})
|
SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.url})
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"name": "Used to distinguish between notification services in case there are multiple Huawei LTE devices configured. Changes to this option value take effect after Home Assistant restart.",
|
"name": "Used to distinguish between notification services in case there are multiple Huawei LTE devices configured. Changes to this option value take effect after Home Assistant restart.",
|
||||||
"recipient": "Comma separated list of default recipient SMS phone numbers for the notification service, used in case the notification sender does not specify any.",
|
"recipient": "Comma-separated list of default recipient SMS phone numbers for the notification service, used in case the notification sender does not specify any.",
|
||||||
"track_wired_clients": "Whether the device tracker entities track also clients attached to the router's wired Ethernet network, in addition to wireless clients.",
|
"track_wired_clients": "Whether the device tracker entities track also clients attached to the router's wired Ethernet network, in addition to wireless clients.",
|
||||||
"unauthenticated_mode": "Whether to run in unauthenticated mode. Unauthenticated mode provides a limited set of features, but may help in case there are problems accessing the router's web interface from a browser while the integration is active. Changes to this option value take effect after integration reload."
|
"unauthenticated_mode": "Whether to run in unauthenticated mode. Unauthenticated mode provides a limited set of features, but may help in case there are problems accessing the router's web interface from a browser while the integration is active. Changes to this option value take effect after integration reload."
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioautomower"],
|
"loggers": ["aioautomower"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aioautomower==2025.4.4"]
|
"requirements": ["aioautomower==2025.5.1"]
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,10 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
HEADLIGHT_MODES: list = [
|
HEADLIGHT_MODES: list = [
|
||||||
HeadlightModes.ALWAYS_OFF.lower(),
|
HeadlightModes.ALWAYS_OFF,
|
||||||
HeadlightModes.ALWAYS_ON.lower(),
|
HeadlightModes.ALWAYS_ON,
|
||||||
HeadlightModes.EVENING_AND_NIGHT.lower(),
|
HeadlightModes.EVENING_AND_NIGHT,
|
||||||
HeadlightModes.EVENING_ONLY.lower(),
|
HeadlightModes.EVENING_ONLY,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -65,13 +65,11 @@ class AutomowerSelectEntity(AutomowerControlEntity, SelectEntity):
|
|||||||
@property
|
@property
|
||||||
def current_option(self) -> str:
|
def current_option(self) -> str:
|
||||||
"""Return the current option for the entity."""
|
"""Return the current option for the entity."""
|
||||||
return cast(
|
return cast(HeadlightModes, self.mower_attributes.settings.headlight.mode)
|
||||||
HeadlightModes, self.mower_attributes.settings.headlight.mode
|
|
||||||
).lower()
|
|
||||||
|
|
||||||
@handle_sending_exception()
|
@handle_sending_exception()
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Change the selected option."""
|
"""Change the selected option."""
|
||||||
await self.coordinator.api.commands.set_headlight_mode(
|
await self.coordinator.api.commands.set_headlight_mode(
|
||||||
self.mower_id, cast(HeadlightModes, option.upper())
|
self.mower_id, HeadlightModes(option)
|
||||||
)
|
)
|
||||||
|
@ -10,6 +10,11 @@ from aiohttp import web
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import http, sensor
|
from homeassistant.components import http, sensor
|
||||||
|
from homeassistant.components.button import (
|
||||||
|
DOMAIN as BUTTON_DOMAIN,
|
||||||
|
SERVICE_PRESS as SERVICE_PRESS_BUTTON,
|
||||||
|
ButtonDeviceClass,
|
||||||
|
)
|
||||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
ATTR_POSITION,
|
ATTR_POSITION,
|
||||||
@ -20,6 +25,7 @@ from homeassistant.components.cover import (
|
|||||||
CoverDeviceClass,
|
CoverDeviceClass,
|
||||||
)
|
)
|
||||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||||
|
from homeassistant.components.input_button import DOMAIN as INPUT_BUTTON_DOMAIN
|
||||||
from homeassistant.components.lock import (
|
from homeassistant.components.lock import (
|
||||||
DOMAIN as LOCK_DOMAIN,
|
DOMAIN as LOCK_DOMAIN,
|
||||||
SERVICE_LOCK,
|
SERVICE_LOCK,
|
||||||
@ -80,6 +86,7 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
ONOFF_DEVICE_CLASSES = {
|
ONOFF_DEVICE_CLASSES = {
|
||||||
|
ButtonDeviceClass,
|
||||||
CoverDeviceClass,
|
CoverDeviceClass,
|
||||||
ValveDeviceClass,
|
ValveDeviceClass,
|
||||||
SwitchDeviceClass,
|
SwitchDeviceClass,
|
||||||
@ -103,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
intent.INTENT_TURN_ON,
|
intent.INTENT_TURN_ON,
|
||||||
HOMEASSISTANT_DOMAIN,
|
HOMEASSISTANT_DOMAIN,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
description="Turns on/opens a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.",
|
description="Turns on/opens/presses a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.",
|
||||||
device_classes=ONOFF_DEVICE_CLASSES,
|
device_classes=ONOFF_DEVICE_CLASSES,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -168,6 +175,25 @@ class OnOffIntentHandler(intent.ServiceIntentHandler):
|
|||||||
"""Call service on entity with handling for special cases."""
|
"""Call service on entity with handling for special cases."""
|
||||||
hass = intent_obj.hass
|
hass = intent_obj.hass
|
||||||
|
|
||||||
|
if state.domain in (BUTTON_DOMAIN, INPUT_BUTTON_DOMAIN):
|
||||||
|
if service != SERVICE_TURN_ON:
|
||||||
|
raise intent.IntentHandleError(
|
||||||
|
f"Entity {state.entity_id} cannot be turned off"
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._run_then_background(
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.services.async_call(
|
||||||
|
state.domain,
|
||||||
|
SERVICE_PRESS_BUTTON,
|
||||||
|
{ATTR_ENTITY_ID: state.entity_id},
|
||||||
|
context=intent_obj.context,
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if state.domain == COVER_DOMAIN:
|
if state.domain == COVER_DOMAIN:
|
||||||
# on = open
|
# on = open
|
||||||
# off = close
|
# off = close
|
||||||
|
@ -401,8 +401,7 @@ def _categorize_programs(isy_data: IsyData, programs: Programs) -> None:
|
|||||||
for dtype, _, node_id in folder.children:
|
for dtype, _, node_id in folder.children:
|
||||||
if dtype != TAG_FOLDER:
|
if dtype != TAG_FOLDER:
|
||||||
continue
|
continue
|
||||||
entity_folder = folder[node_id]
|
entity_folder: Programs = folder[node_id]
|
||||||
|
|
||||||
actions = None
|
actions = None
|
||||||
status = entity_folder.get_by_name(KEY_STATUS)
|
status = entity_folder.get_by_name(KEY_STATUS)
|
||||||
if not status or status.protocol != PROTO_PROGRAM:
|
if not status or status.protocol != PROTO_PROGRAM:
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyisy"],
|
"loggers": ["pyisy"],
|
||||||
"requirements": ["pyisy==3.4.0"],
|
"requirements": ["pyisy==3.4.1"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Universal Devices Inc.",
|
"manufacturer": "Universal Devices Inc.",
|
||||||
|
@ -10,7 +10,9 @@ import voluptuous as vol
|
|||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
@ -62,3 +64,19 @@ class KnockiConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
data_schema=DATA_SCHEMA,
|
data_schema=DATA_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_dhcp(
|
||||||
|
self, discovery_info: DhcpServiceInfo
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a DHCP discovery."""
|
||||||
|
device_registry = dr.async_get(self.hass)
|
||||||
|
if device_entry := device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, discovery_info.hostname)}
|
||||||
|
):
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device_entry.id,
|
||||||
|
new_connections={
|
||||||
|
(dr.CONNECTION_NETWORK_MAC, discovery_info.macaddress)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return await super().async_step_dhcp(discovery_info)
|
||||||
|
@ -3,6 +3,11 @@
|
|||||||
"name": "Knocki",
|
"name": "Knocki",
|
||||||
"codeowners": ["@joostlek", "@jgatto1", "@JakeBosh"],
|
"codeowners": ["@joostlek", "@jgatto1", "@JakeBosh"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
"dhcp": [
|
||||||
|
{
|
||||||
|
"hostname": "knc*"
|
||||||
|
}
|
||||||
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/knocki",
|
"documentation": "https://www.home-assistant.io/integrations/knocki",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
|
@ -50,10 +50,8 @@ rules:
|
|||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: todo
|
diagnostics: todo
|
||||||
discovery-update-info:
|
discovery-update-info: done
|
||||||
status: exempt
|
discovery: done
|
||||||
comment: This is a cloud service and does not benefit from device updates.
|
|
||||||
discovery: todo
|
|
||||||
docs-data-update: todo
|
docs-data-update: todo
|
||||||
docs-examples: todo
|
docs-examples: todo
|
||||||
docs-known-limitations: todo
|
docs-known-limitations: todo
|
||||||
|
@ -152,7 +152,10 @@ async def item_payload(item, get_thumbnail_url=None):
|
|||||||
_LOGGER.debug("Unknown media type received: %s", media_content_type)
|
_LOGGER.debug("Unknown media type received: %s", media_content_type)
|
||||||
raise UnknownMediaType from err
|
raise UnknownMediaType from err
|
||||||
|
|
||||||
thumbnail = item.get("thumbnail")
|
if "art" in item:
|
||||||
|
thumbnail = item["art"].get("poster", item.get("thumbnail"))
|
||||||
|
else:
|
||||||
|
thumbnail = item.get("thumbnail")
|
||||||
if thumbnail is not None and get_thumbnail_url is not None:
|
if thumbnail is not None and get_thumbnail_url is not None:
|
||||||
thumbnail = await get_thumbnail_url(
|
thumbnail = await get_thumbnail_url(
|
||||||
media_content_type, media_content_id, thumbnail_url=thumbnail
|
media_content_type, media_content_id, thumbnail_url=thumbnail
|
||||||
@ -237,14 +240,16 @@ async def get_media_info(media_library, search_id, search_type):
|
|||||||
title = None
|
title = None
|
||||||
media = None
|
media = None
|
||||||
|
|
||||||
properties = ["thumbnail"]
|
properties = ["thumbnail", "art"]
|
||||||
if search_type == MediaType.ALBUM:
|
if search_type == MediaType.ALBUM:
|
||||||
if search_id:
|
if search_id:
|
||||||
album = await media_library.get_album_details(
|
album = await media_library.get_album_details(
|
||||||
album_id=int(search_id), properties=properties
|
album_id=int(search_id), properties=properties
|
||||||
)
|
)
|
||||||
thumbnail = media_library.thumbnail_url(
|
thumbnail = media_library.thumbnail_url(
|
||||||
album["albumdetails"].get("thumbnail")
|
album["albumdetails"]["art"].get(
|
||||||
|
"poster", album["albumdetails"].get("thumbnail")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
title = album["albumdetails"]["label"]
|
title = album["albumdetails"]["label"]
|
||||||
media = await media_library.get_songs(
|
media = await media_library.get_songs(
|
||||||
@ -256,6 +261,7 @@ async def get_media_info(media_library, search_id, search_type):
|
|||||||
"album",
|
"album",
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
"track",
|
"track",
|
||||||
|
"art",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
media = media.get("songs")
|
media = media.get("songs")
|
||||||
@ -274,7 +280,9 @@ async def get_media_info(media_library, search_id, search_type):
|
|||||||
artist_id=int(search_id), properties=properties
|
artist_id=int(search_id), properties=properties
|
||||||
)
|
)
|
||||||
thumbnail = media_library.thumbnail_url(
|
thumbnail = media_library.thumbnail_url(
|
||||||
artist["artistdetails"].get("thumbnail")
|
artist["artistdetails"]["art"].get(
|
||||||
|
"poster", artist["artistdetails"].get("thumbnail")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
title = artist["artistdetails"]["label"]
|
title = artist["artistdetails"]["label"]
|
||||||
else:
|
else:
|
||||||
@ -293,9 +301,10 @@ async def get_media_info(media_library, search_id, search_type):
|
|||||||
movie_id=int(search_id), properties=properties
|
movie_id=int(search_id), properties=properties
|
||||||
)
|
)
|
||||||
thumbnail = media_library.thumbnail_url(
|
thumbnail = media_library.thumbnail_url(
|
||||||
movie["moviedetails"].get("thumbnail")
|
movie["moviedetails"]["art"].get(
|
||||||
|
"poster", movie["moviedetails"].get("thumbnail")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
title = movie["moviedetails"]["label"]
|
|
||||||
else:
|
else:
|
||||||
media = await media_library.get_movies(properties)
|
media = await media_library.get_movies(properties)
|
||||||
media = media.get("movies")
|
media = media.get("movies")
|
||||||
@ -305,14 +314,16 @@ async def get_media_info(media_library, search_id, search_type):
|
|||||||
if search_id:
|
if search_id:
|
||||||
media = await media_library.get_seasons(
|
media = await media_library.get_seasons(
|
||||||
tv_show_id=int(search_id),
|
tv_show_id=int(search_id),
|
||||||
properties=["thumbnail", "season", "tvshowid"],
|
properties=["thumbnail", "season", "tvshowid", "art"],
|
||||||
)
|
)
|
||||||
media = media.get("seasons")
|
media = media.get("seasons")
|
||||||
tvshow = await media_library.get_tv_show_details(
|
tvshow = await media_library.get_tv_show_details(
|
||||||
tv_show_id=int(search_id), properties=properties
|
tv_show_id=int(search_id), properties=properties
|
||||||
)
|
)
|
||||||
thumbnail = media_library.thumbnail_url(
|
thumbnail = media_library.thumbnail_url(
|
||||||
tvshow["tvshowdetails"].get("thumbnail")
|
tvshow["tvshowdetails"]["art"].get(
|
||||||
|
"poster", tvshow["tvshowdetails"].get("thumbnail")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
title = tvshow["tvshowdetails"]["label"]
|
title = tvshow["tvshowdetails"]["label"]
|
||||||
else:
|
else:
|
||||||
@ -325,7 +336,7 @@ async def get_media_info(media_library, search_id, search_type):
|
|||||||
media = await media_library.get_episodes(
|
media = await media_library.get_episodes(
|
||||||
tv_show_id=int(tv_show_id),
|
tv_show_id=int(tv_show_id),
|
||||||
season_id=int(season_id),
|
season_id=int(season_id),
|
||||||
properties=["thumbnail", "tvshowid", "seasonid"],
|
properties=["thumbnail", "tvshowid", "seasonid", "art"],
|
||||||
)
|
)
|
||||||
media = media.get("episodes")
|
media = media.get("episodes")
|
||||||
if media:
|
if media:
|
||||||
@ -333,7 +344,9 @@ async def get_media_info(media_library, search_id, search_type):
|
|||||||
season_id=int(media[0]["seasonid"]), properties=properties
|
season_id=int(media[0]["seasonid"]), properties=properties
|
||||||
)
|
)
|
||||||
thumbnail = media_library.thumbnail_url(
|
thumbnail = media_library.thumbnail_url(
|
||||||
season["seasondetails"].get("thumbnail")
|
season["seasondetails"]["art"].get(
|
||||||
|
"poster", season["seasondetails"].get("thumbnail")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
title = season["seasondetails"]["label"]
|
title = season["seasondetails"]["label"]
|
||||||
|
|
||||||
@ -343,6 +356,7 @@ async def get_media_info(media_library, search_id, search_type):
|
|||||||
properties=["thumbnail", "channeltype", "channel", "broadcastnow"],
|
properties=["thumbnail", "channeltype", "channel", "broadcastnow"],
|
||||||
)
|
)
|
||||||
media = media.get("channels")
|
media = media.get("channels")
|
||||||
|
|
||||||
title = "Channels"
|
title = "Channels"
|
||||||
|
|
||||||
return thumbnail, title, media
|
return thumbnail, title, media
|
||||||
|
@ -23,7 +23,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import issue_registry as ir
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import CONF_USE_BLUETOOTH, DOMAIN
|
from .const import CONF_USE_BLUETOOTH, DOMAIN
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
|||||||
assert entry.unique_id
|
assert entry.unique_id
|
||||||
serial = entry.unique_id
|
serial = entry.unique_id
|
||||||
|
|
||||||
client = async_create_clientsession(hass)
|
client = async_get_clientsession(hass)
|
||||||
cloud_client = LaMarzoccoCloudClient(
|
cloud_client = LaMarzoccoCloudClient(
|
||||||
username=entry.data[CONF_USERNAME],
|
username=entry.data[CONF_USERNAME],
|
||||||
password=entry.data[CONF_PASSWORD],
|
password=entry.data[CONF_PASSWORD],
|
||||||
|
@ -52,7 +52,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
|
|||||||
).status
|
).status
|
||||||
is MachineState.BREWING
|
is MachineState.BREWING
|
||||||
),
|
),
|
||||||
available_fn=lambda device: device.websocket.connected,
|
available_fn=lambda coordinator: not coordinator.websocket_terminated,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
LaMarzoccoBinarySensorEntityDescription(
|
LaMarzoccoBinarySensorEntityDescription(
|
||||||
|
@ -33,7 +33,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.selector import (
|
from homeassistant.helpers.selector import (
|
||||||
SelectOptionDict,
|
SelectOptionDict,
|
||||||
SelectSelector,
|
SelectSelector,
|
||||||
@ -83,7 +83,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
**user_input,
|
**user_input,
|
||||||
}
|
}
|
||||||
|
|
||||||
self._client = async_create_clientsession(self.hass)
|
self._client = async_get_clientsession(self.hass)
|
||||||
cloud_client = LaMarzoccoCloudClient(
|
cloud_client = LaMarzoccoCloudClient(
|
||||||
username=data[CONF_USERNAME],
|
username=data[CONF_USERNAME],
|
||||||
password=data[CONF_PASSWORD],
|
password=data[CONF_PASSWORD],
|
||||||
|
@ -44,6 +44,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
|
|
||||||
_default_update_interval = SCAN_INTERVAL
|
_default_update_interval = SCAN_INTERVAL
|
||||||
config_entry: LaMarzoccoConfigEntry
|
config_entry: LaMarzoccoConfigEntry
|
||||||
|
websocket_terminated = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -92,15 +93,9 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
|||||||
await self.device.get_dashboard()
|
await self.device.get_dashboard()
|
||||||
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
|
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
|
||||||
|
|
||||||
_LOGGER.debug("Init WebSocket in background task")
|
|
||||||
|
|
||||||
self.config_entry.async_create_background_task(
|
self.config_entry.async_create_background_task(
|
||||||
hass=self.hass,
|
hass=self.hass,
|
||||||
target=self.device.connect_dashboard_websocket(
|
target=self.connect_websocket(),
|
||||||
update_callback=lambda _: self.async_set_updated_data(None),
|
|
||||||
connect_callback=self.async_update_listeners,
|
|
||||||
disconnect_callback=self.async_update_listeners,
|
|
||||||
),
|
|
||||||
name="lm_websocket_task",
|
name="lm_websocket_task",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -112,6 +107,23 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
|||||||
)
|
)
|
||||||
self.config_entry.async_on_unload(websocket_close)
|
self.config_entry.async_on_unload(websocket_close)
|
||||||
|
|
||||||
|
async def connect_websocket(self) -> None:
|
||||||
|
"""Connect to the websocket."""
|
||||||
|
|
||||||
|
_LOGGER.debug("Init WebSocket in background task")
|
||||||
|
|
||||||
|
self.websocket_terminated = False
|
||||||
|
self.async_update_listeners()
|
||||||
|
|
||||||
|
await self.device.connect_dashboard_websocket(
|
||||||
|
update_callback=lambda _: self.async_set_updated_data(None),
|
||||||
|
connect_callback=self.async_update_listeners,
|
||||||
|
disconnect_callback=self.async_update_listeners,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.websocket_terminated = True
|
||||||
|
self.async_update_listeners()
|
||||||
|
|
||||||
|
|
||||||
class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||||
"""Coordinator for La Marzocco settings."""
|
"""Coordinator for La Marzocco settings."""
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from pylamarzocco import LaMarzoccoMachine
|
|
||||||
from pylamarzocco.const import FirmwareType
|
from pylamarzocco.const import FirmwareType
|
||||||
|
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_MAC
|
from homeassistant.const import CONF_ADDRESS, CONF_MAC
|
||||||
@ -23,7 +22,7 @@ from .coordinator import LaMarzoccoUpdateCoordinator
|
|||||||
class LaMarzoccoEntityDescription(EntityDescription):
|
class LaMarzoccoEntityDescription(EntityDescription):
|
||||||
"""Description for all LM entities."""
|
"""Description for all LM entities."""
|
||||||
|
|
||||||
available_fn: Callable[[LaMarzoccoMachine], bool] = lambda _: True
|
available_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
|
||||||
supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
|
supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
|
||||||
|
|
||||||
|
|
||||||
@ -74,7 +73,7 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity):
|
|||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
if super().available:
|
if super().available:
|
||||||
return self.entity_description.available_fn(self.coordinator.device)
|
return self.entity_description.available_fn(self.coordinator)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -100,8 +100,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
|||||||
.seconds.seconds_out
|
.seconds.seconds_out
|
||||||
),
|
),
|
||||||
available_fn=(
|
available_fn=(
|
||||||
lambda machine: cast(
|
lambda coordinator: cast(
|
||||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
PreBrewing,
|
||||||
|
coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING],
|
||||||
).mode
|
).mode
|
||||||
is PreExtractionMode.PREINFUSION
|
is PreExtractionMode.PREINFUSION
|
||||||
),
|
),
|
||||||
@ -140,8 +141,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
|||||||
.times.pre_brewing[0]
|
.times.pre_brewing[0]
|
||||||
.seconds.seconds_in
|
.seconds.seconds_in
|
||||||
),
|
),
|
||||||
available_fn=lambda machine: cast(
|
available_fn=lambda coordinator: cast(
|
||||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
PreBrewing, coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||||
).mode
|
).mode
|
||||||
is PreExtractionMode.PREBREWING,
|
is PreExtractionMode.PREBREWING,
|
||||||
supported_fn=(
|
supported_fn=(
|
||||||
@ -180,8 +181,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
|||||||
.seconds.seconds_out
|
.seconds.seconds_out
|
||||||
),
|
),
|
||||||
available_fn=(
|
available_fn=(
|
||||||
lambda machine: cast(
|
lambda coordinator: cast(
|
||||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
PreBrewing,
|
||||||
|
coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING],
|
||||||
).mode
|
).mode
|
||||||
is PreExtractionMode.PREBREWING
|
is PreExtractionMode.PREBREWING
|
||||||
),
|
),
|
||||||
|
@ -442,7 +442,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
|||||||
brightness += params.pop(ATTR_BRIGHTNESS_STEP)
|
brightness += params.pop(ATTR_BRIGHTNESS_STEP)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
brightness += round(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255)
|
brightness_pct = round(brightness / 255 * 100)
|
||||||
|
brightness = round(
|
||||||
|
(brightness_pct + params.pop(ATTR_BRIGHTNESS_STEP_PCT)) / 100 * 255
|
||||||
|
)
|
||||||
|
|
||||||
params[ATTR_BRIGHTNESS] = max(0, min(255, brightness))
|
params[ATTR_BRIGHTNESS] = max(0, min(255, brightness))
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ from .coordinator import LitterRobotDataUpdateCoordinator
|
|||||||
_WhiskerEntityT = TypeVar("_WhiskerEntityT", bound=Robot | Pet)
|
_WhiskerEntityT = TypeVar("_WhiskerEntityT", bound=Robot | Pet)
|
||||||
|
|
||||||
|
|
||||||
def get_device_info(whisker_entity: _WhiskerEntityT) -> DeviceInfo:
|
def get_device_info(whisker_entity: Robot | Pet) -> DeviceInfo:
|
||||||
"""Get device info for a robot or pet."""
|
"""Get device info for a robot or pet."""
|
||||||
if isinstance(whisker_entity, Robot):
|
if isinstance(whisker_entity, Robot):
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
|
@ -36,6 +36,11 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
PRODID = "-//homeassistant.io//local_calendar 1.0//EN"
|
PRODID = "-//homeassistant.io//local_calendar 1.0//EN"
|
||||||
|
|
||||||
|
# The calendar on disk is only changed when this entity is updated, so there
|
||||||
|
# is no need to poll for changes. The calendar enttiy base class will handle
|
||||||
|
# refreshing the entity state based on the start or end time of the event.
|
||||||
|
SCAN_INTERVAL = timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -89,20 +94,27 @@ class LocalCalendarEntity(CalendarEntity):
|
|||||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||||
) -> list[CalendarEvent]:
|
) -> list[CalendarEvent]:
|
||||||
"""Get all events in a specific time frame."""
|
"""Get all events in a specific time frame."""
|
||||||
events = self._calendar.timeline_tz(start_date.tzinfo).overlapping(
|
|
||||||
start_date,
|
def events_in_range() -> list[CalendarEvent]:
|
||||||
end_date,
|
events = self._calendar.timeline_tz(start_date.tzinfo).overlapping(
|
||||||
)
|
start_date,
|
||||||
return [_get_calendar_event(event) for event in events]
|
end_date,
|
||||||
|
)
|
||||||
|
return [_get_calendar_event(event) for event in events]
|
||||||
|
|
||||||
|
return await self.hass.async_add_executor_job(events_in_range)
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update entity state with the next upcoming event."""
|
"""Update entity state with the next upcoming event."""
|
||||||
now = dt_util.now()
|
|
||||||
events = self._calendar.timeline_tz(now.tzinfo).active_after(now)
|
def next_event() -> CalendarEvent | None:
|
||||||
if event := next(events, None):
|
now = dt_util.now()
|
||||||
self._event = _get_calendar_event(event)
|
events = self._calendar.timeline_tz(now.tzinfo).active_after(now)
|
||||||
else:
|
if event := next(events, None):
|
||||||
self._event = None
|
return _get_calendar_event(event)
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._event = await self.hass.async_add_executor_job(next_event)
|
||||||
|
|
||||||
async def _async_store(self) -> None:
|
async def _async_store(self) -> None:
|
||||||
"""Persist the calendar to disk."""
|
"""Persist the calendar to disk."""
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["ical"],
|
"loggers": ["ical"],
|
||||||
"requirements": ["ical==9.1.0"]
|
"requirements": ["ical==9.2.0"]
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["ical==9.1.0"]
|
"requirements": ["ical==9.2.0"]
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,11 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
|||||||
t_key = ZONE1_DEVICES.get(
|
t_key = ZONE1_DEVICES.get(
|
||||||
cast(MieleAppliance, self.device.device_type), "zone_1"
|
cast(MieleAppliance, self.device.device_type), "zone_1"
|
||||||
)
|
)
|
||||||
|
if self.device.device_type in (
|
||||||
|
MieleAppliance.FRIDGE,
|
||||||
|
MieleAppliance.FREEZER,
|
||||||
|
):
|
||||||
|
self._attr_name = None
|
||||||
|
|
||||||
if description.zone == 2:
|
if description.zone == 2:
|
||||||
if self.device.device_type in (
|
if self.device.device_type in (
|
||||||
@ -192,8 +197,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
|||||||
@property
|
@property
|
||||||
def target_temperature(self) -> float | None:
|
def target_temperature(self) -> float | None:
|
||||||
"""Return the target temperature."""
|
"""Return the target temperature."""
|
||||||
if self.entity_description.target_fn(self.device) is None:
|
|
||||||
return None
|
|
||||||
return cast(float | None, self.entity_description.target_fn(self.device))
|
return cast(float | None, self.entity_description.target_fn(self.device))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
|
from pymiele import MieleEnum
|
||||||
|
|
||||||
DOMAIN = "miele"
|
DOMAIN = "miele"
|
||||||
MANUFACTURER = "Miele"
|
MANUFACTURER = "Miele"
|
||||||
|
|
||||||
@ -325,13 +327,17 @@ STATE_PROGRAM_PHASE: dict[int, dict[int, str]] = {
|
|||||||
MieleAppliance.ROBOT_VACUUM_CLEANER: STATE_PROGRAM_PHASE_ROBOT_VACUUM_CLEANER,
|
MieleAppliance.ROBOT_VACUUM_CLEANER: STATE_PROGRAM_PHASE_ROBOT_VACUUM_CLEANER,
|
||||||
}
|
}
|
||||||
|
|
||||||
STATE_PROGRAM_TYPE = {
|
|
||||||
0: "normal_operation_mode",
|
class StateProgramType(MieleEnum):
|
||||||
1: "own_program",
|
"""Defines program types."""
|
||||||
2: "automatic_program",
|
|
||||||
3: "cleaning_care_program",
|
normal_operation_mode = 0
|
||||||
4: "maintenance_program",
|
own_program = 1
|
||||||
}
|
automatic_program = 2
|
||||||
|
cleaning_care_program = 3
|
||||||
|
maintenance_program = 4
|
||||||
|
unknown = -9999
|
||||||
|
|
||||||
|
|
||||||
WASHING_MACHINE_PROGRAM_ID: dict[int, str] = {
|
WASHING_MACHINE_PROGRAM_ID: dict[int, str] = {
|
||||||
-1: "no_program", # Extrapolated from other device types.
|
-1: "no_program", # Extrapolated from other device types.
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pymiele"],
|
"loggers": ["pymiele"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pymiele==0.4.1"],
|
"requirements": ["pymiele==0.4.3"],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"zeroconf": ["_mieleathome._tcp.local."]
|
"zeroconf": ["_mieleathome._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -30,9 +30,9 @@ from homeassistant.helpers.typing import StateType
|
|||||||
from .const import (
|
from .const import (
|
||||||
STATE_PROGRAM_ID,
|
STATE_PROGRAM_ID,
|
||||||
STATE_PROGRAM_PHASE,
|
STATE_PROGRAM_PHASE,
|
||||||
STATE_PROGRAM_TYPE,
|
|
||||||
STATE_STATUS_TAGS,
|
STATE_STATUS_TAGS,
|
||||||
MieleAppliance,
|
MieleAppliance,
|
||||||
|
StateProgramType,
|
||||||
StateStatus,
|
StateStatus,
|
||||||
)
|
)
|
||||||
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
|
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
|
||||||
@ -181,10 +181,10 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
|||||||
description=MieleSensorDescription(
|
description=MieleSensorDescription(
|
||||||
key="state_program_type",
|
key="state_program_type",
|
||||||
translation_key="program_type",
|
translation_key="program_type",
|
||||||
value_fn=lambda value: value.state_program_type,
|
value_fn=lambda value: StateProgramType(value.state_program_type).name,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=sorted(set(STATE_PROGRAM_TYPE.values())),
|
options=sorted(set(StateProgramType.keys())),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MieleSensorDefinition(
|
MieleSensorDefinition(
|
||||||
@ -440,8 +440,6 @@ async def async_setup_entry(
|
|||||||
entity_class = MieleProgramIdSensor
|
entity_class = MieleProgramIdSensor
|
||||||
case "state_program_phase":
|
case "state_program_phase":
|
||||||
entity_class = MielePhaseSensor
|
entity_class = MielePhaseSensor
|
||||||
case "state_program_type":
|
|
||||||
entity_class = MieleTypeSensor
|
|
||||||
case _:
|
case _:
|
||||||
entity_class = MieleSensor
|
entity_class = MieleSensor
|
||||||
if (
|
if (
|
||||||
@ -553,22 +551,6 @@ class MielePhaseSensor(MieleSensor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MieleTypeSensor(MieleSensor):
|
|
||||||
"""Representation of the program type sensor."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> StateType:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
ret_val = STATE_PROGRAM_TYPE.get(int(self.device.state_program_type))
|
|
||||||
if ret_val is None:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Unknown program type: %s on device type: %s",
|
|
||||||
self.device.state_program_type,
|
|
||||||
self.device.device_type,
|
|
||||||
)
|
|
||||||
return ret_val
|
|
||||||
|
|
||||||
|
|
||||||
class MieleProgramIdSensor(MieleSensor):
|
class MieleProgramIdSensor(MieleSensor):
|
||||||
"""Representation of the program id sensor."""
|
"""Representation of the program id sensor."""
|
||||||
|
|
||||||
|
@ -244,7 +244,6 @@
|
|||||||
"title": "Configure MQTT device \"{mqtt_device}\"",
|
"title": "Configure MQTT device \"{mqtt_device}\"",
|
||||||
"description": "Please configure MQTT specific details for {platform} entity \"{entity}\":",
|
"description": "Please configure MQTT specific details for {platform} entity \"{entity}\":",
|
||||||
"data": {
|
"data": {
|
||||||
"on_command_type": "ON command type",
|
|
||||||
"blue_template": "Blue template",
|
"blue_template": "Blue template",
|
||||||
"brightness_template": "Brightness template",
|
"brightness_template": "Brightness template",
|
||||||
"command_template": "Command template",
|
"command_template": "Command template",
|
||||||
@ -255,9 +254,10 @@
|
|||||||
"force_update": "Force update",
|
"force_update": "Force update",
|
||||||
"green_template": "Green template",
|
"green_template": "Green template",
|
||||||
"last_reset_value_template": "Last reset value template",
|
"last_reset_value_template": "Last reset value template",
|
||||||
|
"on_command_type": "ON command type",
|
||||||
"optimistic": "Optimistic",
|
"optimistic": "Optimistic",
|
||||||
"payload_off": "Payload off",
|
"payload_off": "Payload \"off\"",
|
||||||
"payload_on": "Payload on",
|
"payload_on": "Payload \"on\"",
|
||||||
"qos": "QoS",
|
"qos": "QoS",
|
||||||
"red_template": "Red template",
|
"red_template": "Red template",
|
||||||
"retain": "Retain",
|
"retain": "Retain",
|
||||||
@ -275,19 +275,19 @@
|
|||||||
"command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic.",
|
"command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic.",
|
||||||
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)",
|
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)",
|
||||||
"color_temp_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract color temperature in Kelvin from the state payload value. Expected result of the template is an integer.",
|
"color_temp_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract color temperature in Kelvin from the state payload value. Expected result of the template is an integer.",
|
||||||
|
"force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)",
|
||||||
"green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.",
|
"green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.",
|
||||||
"last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)",
|
"last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)",
|
||||||
"force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)",
|
"on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.",
|
||||||
"on_command_type": "Defines when the `payload on` is sent. Using `last` (the default) will send any style (brightness, color, etc) topics first and then a `payload on` to the command_topic. Using `first` will send the `payload on` and then any style topics. Using `brightness` will only send brightness commands instead of the `Payload on` to turn the light on.",
|
|
||||||
"optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)",
|
"optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)",
|
||||||
"payload_off": "The payload that represents the off state.",
|
"payload_off": "The payload that represents the \"off\" state.",
|
||||||
"payload_on": "The payload that represents the on state.",
|
"payload_on": "The payload that represents the \"on\" state.",
|
||||||
"qos": "The QoS value a {platform} entity should use.",
|
"qos": "The QoS value a {platform} entity should use.",
|
||||||
"red_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.",
|
"red_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.",
|
||||||
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.",
|
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.",
|
||||||
"state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.",
|
"state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.",
|
||||||
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)",
|
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)",
|
||||||
"supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, WHITE. Note that if onoff or brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
|
"supported_color_modes": "A list of color modes supported by the light. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
|
||||||
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)"
|
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)"
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
@ -325,7 +325,7 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"brightness": "Flag that defines if light supports brightness when the RGB, RGBW, or RGBWW color mode is supported.",
|
"brightness": "Flag that defines if light supports brightness when the RGB, RGBW, or RGBWW color mode is supported.",
|
||||||
"brightness_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the brightness command topic.",
|
"brightness_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the brightness command topic.",
|
||||||
"brightness_command_topic": "The publishing topic that will be used to control the brigthness. [Learn more.]({url}#brightness_command_topic)",
|
"brightness_command_topic": "The publishing topic that will be used to control the brightness. [Learn more.]({url}#brightness_command_topic)",
|
||||||
"brightness_scale": "Defines the maximum brightness value (i.e., 100%) of the maximum brightness.",
|
"brightness_scale": "Defines the maximum brightness value (i.e., 100%) of the maximum brightness.",
|
||||||
"brightness_state_topic": "The MQTT topic subscribed to receive brightness state values. [Learn more.]({url}#brightness_state_topic)",
|
"brightness_state_topic": "The MQTT topic subscribed to receive brightness state values. [Learn more.]({url}#brightness_state_topic)",
|
||||||
"brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value."
|
"brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value."
|
||||||
@ -385,7 +385,7 @@
|
|||||||
"hs_value_template": "HS value template"
|
"hs_value_template": "HS value template"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to hs_command_topic. Available variables: `hue` and `sat`.",
|
"hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to HS command topic. Available variables: `hue` and `sat`.",
|
||||||
"hs_command_topic": "The MQTT topic to publish commands to change the light’s color state in HS format (Hue Saturation). Range for Hue: 0° .. 360°, Range of Saturation: 0..100. Note: Brightness is sent separately in the brightness command topic. [Learn more.]({url}#hs_command_topic)",
|
"hs_command_topic": "The MQTT topic to publish commands to change the light’s color state in HS format (Hue Saturation). Range for Hue: 0° .. 360°, Range of Saturation: 0..100. Note: Brightness is sent separately in the brightness command topic. [Learn more.]({url}#hs_command_topic)",
|
||||||
"hs_state_topic": "The MQTT topic subscribed to receive color state updates in HS format. The expected payload is the hue and saturation values separated by commas, for example, `359.5,100.0`. Note: Brightness is received separately in the brightness state topic. [Learn more.]({url}#hs_state_topic)",
|
"hs_state_topic": "The MQTT topic subscribed to receive color state updates in HS format. The expected payload is the hue and saturation values separated by commas, for example, `359.5,100.0`. Note: Brightness is received separately in the brightness state topic. [Learn more.]({url}#hs_state_topic)",
|
||||||
"hs_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the HS value."
|
"hs_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the HS value."
|
||||||
@ -574,15 +574,15 @@
|
|||||||
"discovery": "Option to enable MQTT automatic discovery.",
|
"discovery": "Option to enable MQTT automatic discovery.",
|
||||||
"discovery_prefix": "The prefix of configuration topics the MQTT integration will subscribe to.",
|
"discovery_prefix": "The prefix of configuration topics the MQTT integration will subscribe to.",
|
||||||
"birth_enable": "When set, Home Assistant will publish an online message to your MQTT broker when MQTT is ready.",
|
"birth_enable": "When set, Home Assistant will publish an online message to your MQTT broker when MQTT is ready.",
|
||||||
"birth_topic": "The MQTT topic where Home Assistant will publish a `birth` message.",
|
"birth_topic": "The MQTT topic where Home Assistant will publish a \"birth\" message.",
|
||||||
"birth_payload": "The `birth` message that is published when MQTT is ready and connected.",
|
"birth_payload": "The \"birth\" message that is published when MQTT is ready and connected.",
|
||||||
"birth_qos": "The quality of service of the `birth` message that is published when MQTT is ready and connected",
|
"birth_qos": "The quality of service of the \"birth\" message that is published when MQTT is ready and connected",
|
||||||
"birth_retain": "When set, Home Assistant will retain the `birth` message published to your MQTT broker.",
|
"birth_retain": "When set, Home Assistant will retain the \"birth\" message published to your MQTT broker.",
|
||||||
"will_enable": "When set, Home Assistant will ask your broker to publish a `will` message when MQTT is stopped or when it loses the connection to your broker.",
|
"will_enable": "When set, Home Assistant will ask your broker to publish a \"will\" message when MQTT is stopped or when it loses the connection to your broker.",
|
||||||
"will_topic": "The MQTT topic your MQTT broker will publish a `will` message to.",
|
"will_topic": "The MQTT topic your MQTT broker will publish a \"will\" message to.",
|
||||||
"will_payload": "The message your MQTT broker `will` publish when the MQTT integration is stopped or when the connection is lost.",
|
"will_payload": "The message your MQTT broker \"will\" publish when the MQTT integration is stopped or when the connection is lost.",
|
||||||
"will_qos": "The quality of service of the `will` message that is published by your MQTT broker.",
|
"will_qos": "The quality of service of the \"will\" message that is published by your MQTT broker.",
|
||||||
"will_retain": "When set, your MQTT broker will retain the `will` message."
|
"will_retain": "When set, your MQTT broker will retain the \"will\" message."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -12,5 +12,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["nexia"],
|
"loggers": ["nexia"],
|
||||||
"requirements": ["nexia==2.7.0"]
|
"requirements": ["nexia==2.9.0"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["py_nextbus"],
|
"loggers": ["py_nextbus"],
|
||||||
"requirements": ["py-nextbusnext==2.0.5"]
|
"requirements": ["py-nextbusnext==2.1.2"]
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,9 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP addresses (192.168.1.1), IP networks (192.168.0.0/24) or IP ranges (192.168.1.0-32).",
|
"description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP addresses (192.168.1.1), IP networks (192.168.0.0/24) or IP ranges (192.168.1.0-32).",
|
||||||
"data": {
|
"data": {
|
||||||
"hosts": "Network addresses (comma separated) to scan",
|
"hosts": "Network addresses (comma-separated) to scan",
|
||||||
"home_interval": "Minimum number of minutes between scans of active devices (preserve battery)",
|
"home_interval": "Minimum number of minutes between scans of active devices (preserve battery)",
|
||||||
"exclude": "Network addresses (comma separated) to exclude from scanning",
|
"exclude": "Network addresses (comma-separated) to exclude from scanning",
|
||||||
"scan_options": "Raw configurable scan options for Nmap"
|
"scan_options": "Raw configurable scan options for Nmap"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,14 +113,16 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
_LOGGER.error("Error getting accounts: %s", err)
|
_LOGGER.error("Error getting accounts: %s", err)
|
||||||
raise
|
raise
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
id_prefix = "_".join(
|
id_prefix = (
|
||||||
(
|
(
|
||||||
self.api.utility.subdomain(),
|
f"{self.api.utility.subdomain()}_{account.meter_type.name}_"
|
||||||
account.meter_type.name.lower(),
|
f"{account.utility_account_id}"
|
||||||
# Some utilities like AEP have "-" in their account id.
|
|
||||||
# Replace it with "_" to avoid "Invalid statistic_id"
|
|
||||||
account.utility_account_id.replace("-", "_").lower(),
|
|
||||||
)
|
)
|
||||||
|
# Some utilities like AEP have "-" in their account id.
|
||||||
|
# Other utilities like ngny-gas have "-" in their subdomain.
|
||||||
|
# Replace it with "_" to avoid "Invalid statistic_id"
|
||||||
|
.replace("-", "_")
|
||||||
|
.lower()
|
||||||
)
|
)
|
||||||
cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost"
|
cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost"
|
||||||
compensation_statistic_id = f"{DOMAIN}:{id_prefix}_energy_compensation"
|
compensation_statistic_id = f"{DOMAIN}:{id_prefix}_energy_compensation"
|
||||||
@ -190,7 +192,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
return_sum = 0.0
|
return_sum = 0.0
|
||||||
last_stats_time = None
|
last_stats_time = None
|
||||||
else:
|
else:
|
||||||
await self._async_maybe_migrate_statistics(
|
migrated = await self._async_maybe_migrate_statistics(
|
||||||
account.utility_account_id,
|
account.utility_account_id,
|
||||||
{
|
{
|
||||||
cost_statistic_id: compensation_statistic_id,
|
cost_statistic_id: compensation_statistic_id,
|
||||||
@ -203,6 +205,13 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
return_statistic_id: return_metadata,
|
return_statistic_id: return_metadata,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if migrated:
|
||||||
|
# Skip update to avoid working on old data since the migration is done
|
||||||
|
# asynchronously. Update the statistics in the next refresh in 12h.
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Statistics migration completed. Skipping update for now"
|
||||||
|
)
|
||||||
|
continue
|
||||||
cost_reads = await self._async_get_cost_reads(
|
cost_reads = await self._async_get_cost_reads(
|
||||||
account,
|
account,
|
||||||
self.api.utility.timezone(),
|
self.api.utility.timezone(),
|
||||||
@ -326,7 +335,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
utility_account_id: str,
|
utility_account_id: str,
|
||||||
migration_map: dict[str, str],
|
migration_map: dict[str, str],
|
||||||
metadata_map: dict[str, StatisticMetaData],
|
metadata_map: dict[str, StatisticMetaData],
|
||||||
) -> None:
|
) -> bool:
|
||||||
"""Perform one-time statistics migration based on the provided map.
|
"""Perform one-time statistics migration based on the provided map.
|
||||||
|
|
||||||
Splits negative values from source IDs into target IDs.
|
Splits negative values from source IDs into target IDs.
|
||||||
@ -339,7 +348,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if not migration_map:
|
if not migration_map:
|
||||||
return
|
return False
|
||||||
|
|
||||||
need_migration_source_ids = set()
|
need_migration_source_ids = set()
|
||||||
for source_id, target_id in migration_map.items():
|
for source_id, target_id in migration_map.items():
|
||||||
@ -354,7 +363,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
if not last_target_stat:
|
if not last_target_stat:
|
||||||
need_migration_source_ids.add(source_id)
|
need_migration_source_ids.add(source_id)
|
||||||
if not need_migration_source_ids:
|
if not need_migration_source_ids:
|
||||||
return
|
return False
|
||||||
|
|
||||||
_LOGGER.info("Starting one-time migration for: %s", need_migration_source_ids)
|
_LOGGER.info("Starting one-time migration for: %s", need_migration_source_ids)
|
||||||
|
|
||||||
@ -416,7 +425,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
|
|
||||||
if not need_migration_source_ids:
|
if not need_migration_source_ids:
|
||||||
_LOGGER.debug("No migration needed")
|
_LOGGER.debug("No migration needed")
|
||||||
return
|
return False
|
||||||
|
|
||||||
for stat_id, stats in processed_stats.items():
|
for stat_id, stats in processed_stats.items():
|
||||||
_LOGGER.debug("Applying %d migrated stats for %s", len(stats), stat_id)
|
_LOGGER.debug("Applying %d migrated stats for %s", len(stats), stat_id)
|
||||||
@ -434,7 +443,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
"energy_settings": "/config/energy",
|
"energy_settings": "/config/energy",
|
||||||
"target_ids": "\n".join(
|
"target_ids": "\n".join(
|
||||||
{
|
{
|
||||||
v
|
str(metadata_map[v]["name"])
|
||||||
for k, v in migration_map.items()
|
for k, v in migration_map.items()
|
||||||
if k in need_migration_source_ids
|
if k in need_migration_source_ids
|
||||||
}
|
}
|
||||||
@ -442,6 +451,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
async def _async_get_cost_reads(
|
async def _async_get_cost_reads(
|
||||||
self, account: Account, time_zone_str: str, start_time: float | None = None
|
self, account: Account, time_zone_str: str, start_time: float | None = None
|
||||||
) -> list[CostRead]:
|
) -> list[CostRead]:
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
"issues": {
|
"issues": {
|
||||||
"return_to_grid_migration": {
|
"return_to_grid_migration": {
|
||||||
"title": "Return to grid statistics for account: {utility_account_id}",
|
"title": "Return to grid statistics for account: {utility_account_id}",
|
||||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}"
|
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"county": "County",
|
"county": "County",
|
||||||
"phone_number": "Phone Number"
|
"phone_number": "Phone number"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"county": "County used for outage number retrieval",
|
"county": "County used for outage number retrieval",
|
||||||
|
@ -56,10 +56,15 @@ class PicoProvider(Provider):
|
|||||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf:
|
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf:
|
||||||
fname = tmpf.name
|
fname = tmpf.name
|
||||||
|
|
||||||
cmd = ["pico2wave", "--wave", fname, "-l", language, "--", message]
|
cmd = ["pico2wave", "--wave", fname, "-l", language]
|
||||||
subprocess.call(cmd)
|
result = subprocess.run(cmd, text=True, input=message, check=False)
|
||||||
data = None
|
data = None
|
||||||
try:
|
try:
|
||||||
|
if result.returncode != 0:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error running pico2wave, return code: %s", result.returncode
|
||||||
|
)
|
||||||
|
return (None, None)
|
||||||
with open(fname, "rb") as voice:
|
with open(fname, "rb") as voice:
|
||||||
data = voice.read()
|
data = voice.read()
|
||||||
except OSError:
|
except OSError:
|
||||||
|
@ -40,7 +40,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
|||||||
)
|
)
|
||||||
|
|
||||||
rehlko.set_refresh_token_callback(async_refresh_token_update)
|
rehlko.set_refresh_token_callback(async_refresh_token_update)
|
||||||
rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20])
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await rehlko.authenticate(
|
await rehlko.authenticate(
|
||||||
@ -48,6 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
|||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
entry.data.get(CONF_REFRESH_TOKEN),
|
entry.data.get(CONF_REFRESH_TOKEN),
|
||||||
)
|
)
|
||||||
|
homes = await rehlko.get_homes()
|
||||||
except AuthenticationError as ex:
|
except AuthenticationError as ex:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
@ -60,7 +60,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
|||||||
translation_key="cannot_connect",
|
translation_key="cannot_connect",
|
||||||
) from ex
|
) from ex
|
||||||
coordinators: dict[int, RehlkoUpdateCoordinator] = {}
|
coordinators: dict[int, RehlkoUpdateCoordinator] = {}
|
||||||
homes = await rehlko.get_homes()
|
|
||||||
|
|
||||||
entry.runtime_data = RehlkoRuntimeData(
|
entry.runtime_data = RehlkoRuntimeData(
|
||||||
coordinators=coordinators,
|
coordinators=coordinators,
|
||||||
@ -86,6 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
|||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
coordinators[device_id] = coordinator
|
coordinators[device_id] = coordinator
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
# Retrys enabled after successful connection to prevent blocking startup
|
||||||
|
rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,5 +13,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aiokem"],
|
"loggers": ["aiokem"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aiokem==0.5.6"]
|
"requirements": ["aiokem==0.5.10"]
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ async def async_setup_entry(
|
|||||||
"""Set up the remote calendar platform."""
|
"""Set up the remote calendar platform."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
entity = RemoteCalendarEntity(coordinator, entry)
|
entity = RemoteCalendarEntity(coordinator, entry)
|
||||||
async_add_entities([entity])
|
async_add_entities([entity], True)
|
||||||
|
|
||||||
|
|
||||||
class RemoteCalendarEntity(
|
class RemoteCalendarEntity(
|
||||||
@ -48,25 +48,46 @@ class RemoteCalendarEntity(
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_name = entry.data[CONF_CALENDAR_NAME]
|
self._attr_name = entry.data[CONF_CALENDAR_NAME]
|
||||||
self._attr_unique_id = entry.entry_id
|
self._attr_unique_id = entry.entry_id
|
||||||
|
self._event: CalendarEvent | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event(self) -> CalendarEvent | None:
|
def event(self) -> CalendarEvent | None:
|
||||||
"""Return the next upcoming event."""
|
"""Return the next upcoming event."""
|
||||||
now = dt_util.now()
|
return self._event
|
||||||
events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now)
|
|
||||||
if event := next(events, None):
|
|
||||||
return _get_calendar_event(event)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def async_get_events(
|
async def async_get_events(
|
||||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||||
) -> list[CalendarEvent]:
|
) -> list[CalendarEvent]:
|
||||||
"""Get all events in a specific time frame."""
|
"""Get all events in a specific time frame."""
|
||||||
events = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping(
|
|
||||||
start_date,
|
def events_in_range() -> list[CalendarEvent]:
|
||||||
end_date,
|
"""Return all events in the given time range."""
|
||||||
)
|
events = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping(
|
||||||
return [_get_calendar_event(event) for event in events]
|
start_date,
|
||||||
|
end_date,
|
||||||
|
)
|
||||||
|
return [_get_calendar_event(event) for event in events]
|
||||||
|
|
||||||
|
return await self.hass.async_add_executor_job(events_in_range)
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Refresh the timeline.
|
||||||
|
|
||||||
|
This is called when the coordinator updates. Creating the timeline may
|
||||||
|
require walking through the entire calendar and handling recurring
|
||||||
|
events, so it is done as a separate task without blocking the event loop.
|
||||||
|
"""
|
||||||
|
await super().async_update()
|
||||||
|
|
||||||
|
def next_timeline_event() -> CalendarEvent | None:
|
||||||
|
"""Return the next active event."""
|
||||||
|
now = dt_util.now()
|
||||||
|
events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now)
|
||||||
|
if event := next(events, None):
|
||||||
|
return _get_calendar_event(event)
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._event = await self.hass.async_add_executor_job(next_timeline_event)
|
||||||
|
|
||||||
|
|
||||||
def _get_calendar_event(event: Event) -> CalendarEvent:
|
def _get_calendar_event(event: Event) -> CalendarEvent:
|
||||||
|
@ -5,8 +5,6 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from httpx import HTTPError, InvalidURL
|
from httpx import HTTPError, InvalidURL
|
||||||
from ical.calendar_stream import IcsCalendarStream
|
|
||||||
from ical.exceptions import CalendarParseError
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
@ -14,6 +12,7 @@ from homeassistant.const import CONF_URL
|
|||||||
from homeassistant.helpers.httpx_client import get_async_client
|
from homeassistant.helpers.httpx_client import get_async_client
|
||||||
|
|
||||||
from .const import CONF_CALENDAR_NAME, DOMAIN
|
from .const import CONF_CALENDAR_NAME, DOMAIN
|
||||||
|
from .ics import InvalidIcsException, parse_calendar
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -64,15 +63,9 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.debug("An error occurred: %s", err)
|
_LOGGER.debug("An error occurred: %s", err)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_executor_job(
|
await parse_calendar(self.hass, res.text)
|
||||||
IcsCalendarStream.calendar_from_ics, res.text
|
except InvalidIcsException:
|
||||||
)
|
|
||||||
except CalendarParseError as err:
|
|
||||||
errors["base"] = "invalid_ics_file"
|
errors["base"] = "invalid_ics_file"
|
||||||
_LOGGER.error("Error reading the calendar information: %s", err.message)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Additional calendar error detail: %s", str(err.detailed_error)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user_input[CONF_CALENDAR_NAME], data=user_input
|
title=user_input[CONF_CALENDAR_NAME], data=user_input
|
||||||
|
@ -5,8 +5,6 @@ import logging
|
|||||||
|
|
||||||
from httpx import HTTPError, InvalidURL
|
from httpx import HTTPError, InvalidURL
|
||||||
from ical.calendar import Calendar
|
from ical.calendar import Calendar
|
||||||
from ical.calendar_stream import IcsCalendarStream
|
|
||||||
from ical.exceptions import CalendarParseError
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_URL
|
from homeassistant.const import CONF_URL
|
||||||
@ -15,6 +13,7 @@ from homeassistant.helpers.httpx_client import get_async_client
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .ics import InvalidIcsException, parse_calendar
|
||||||
|
|
||||||
type RemoteCalendarConfigEntry = ConfigEntry[RemoteCalendarDataUpdateCoordinator]
|
type RemoteCalendarConfigEntry = ConfigEntry[RemoteCalendarDataUpdateCoordinator]
|
||||||
|
|
||||||
@ -56,14 +55,9 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
|
|||||||
translation_placeholders={"err": str(err)},
|
translation_placeholders={"err": str(err)},
|
||||||
) from err
|
) from err
|
||||||
try:
|
try:
|
||||||
# calendar_from_ics will dynamically load packages
|
|
||||||
# the first time it is called, so we need to do it
|
|
||||||
# in a separate thread to avoid blocking the event loop
|
|
||||||
self.ics = res.text
|
self.ics = res.text
|
||||||
return await self.hass.async_add_executor_job(
|
return await parse_calendar(self.hass, res.text)
|
||||||
IcsCalendarStream.calendar_from_ics, self.ics
|
except InvalidIcsException as err:
|
||||||
)
|
|
||||||
except CalendarParseError as err:
|
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="unable_to_parse",
|
translation_key="unable_to_parse",
|
||||||
|
44
homeassistant/components/remote_calendar/ics.py
Normal file
44
homeassistant/components/remote_calendar/ics.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""Module for parsing ICS content.
|
||||||
|
|
||||||
|
This module exists to fix known issues where calendar providers return calendars
|
||||||
|
that do not follow rfcc5545. This module will attempt to fix the calendar and return
|
||||||
|
a valid calendar object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ical.calendar import Calendar
|
||||||
|
from ical.calendar_stream import IcsCalendarStream
|
||||||
|
from ical.compat import enable_compat_mode
|
||||||
|
from ical.exceptions import CalendarParseError
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidIcsException(Exception):
|
||||||
|
"""Exception to indicate that the ICS content is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
def _compat_calendar_from_ics(ics: str) -> Calendar:
|
||||||
|
"""Parse the ICS content and return a Calendar object.
|
||||||
|
|
||||||
|
This function is called in a separate thread to avoid blocking the event
|
||||||
|
loop while loading packages or parsing the ICS content for large calendars.
|
||||||
|
|
||||||
|
It uses the `enable_compat_mode` context manager to fix known issues with
|
||||||
|
calendar providers that return invalid calendars.
|
||||||
|
"""
|
||||||
|
with enable_compat_mode(ics) as compat_ics:
|
||||||
|
return IcsCalendarStream.calendar_from_ics(compat_ics)
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_calendar(hass: HomeAssistant, ics: str) -> Calendar:
|
||||||
|
"""Parse the ICS content and return a Calendar object."""
|
||||||
|
try:
|
||||||
|
return await hass.async_add_executor_job(_compat_calendar_from_ics, ics)
|
||||||
|
except CalendarParseError as err:
|
||||||
|
_LOGGER.error("Error parsing calendar information: %s", err.message)
|
||||||
|
_LOGGER.debug("Additional calendar error detail: %s", str(err.detailed_error))
|
||||||
|
raise InvalidIcsException(err.message) from err
|
@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["ical"],
|
"loggers": ["ical"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["ical==9.1.0"]
|
"requirements": ["ical==9.2.0"]
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Sign-in with Ring account",
|
"title": "Sign in with Ring account",
|
||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
|||||||
)
|
)
|
||||||
_LOGGER.debug("Getting home data")
|
_LOGGER.debug("Getting home data")
|
||||||
try:
|
try:
|
||||||
home_data = await api_client.get_home_data_v2(user_data)
|
home_data = await api_client.get_home_data_v3(user_data)
|
||||||
except RoborockInvalidCredentials as err:
|
except RoborockInvalidCredentials as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
"Invalid credentials",
|
"Invalid credentials",
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"loggers": ["roborock"],
|
"loggers": ["roborock"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"python-roborock==2.16.1",
|
"python-roborock==2.18.2",
|
||||||
"vacuum-map-parser-roborock==0.1.2"
|
"vacuum-map-parser-roborock==0.1.4"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ from typing import cast
|
|||||||
|
|
||||||
from aiobotocore.client import AioBaseClient as S3Client
|
from aiobotocore.client import AioBaseClient as S3Client
|
||||||
from aiobotocore.session import AioSession
|
from aiobotocore.session import AioSession
|
||||||
|
from botocore.config import Config
|
||||||
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
|
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -32,6 +33,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
|||||||
"""Set up S3 from a config entry."""
|
"""Set up S3 from a config entry."""
|
||||||
|
|
||||||
data = cast(dict, entry.data)
|
data = cast(dict, entry.data)
|
||||||
|
# due to https://github.com/home-assistant/core/issues/143995
|
||||||
|
config = Config(
|
||||||
|
request_checksum_calculation="when_required",
|
||||||
|
response_checksum_validation="when_required",
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
session = AioSession()
|
session = AioSession()
|
||||||
# pylint: disable-next=unnecessary-dunder-call
|
# pylint: disable-next=unnecessary-dunder-call
|
||||||
@ -40,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
|||||||
endpoint_url=data.get(CONF_ENDPOINT_URL),
|
endpoint_url=data.get(CONF_ENDPOINT_URL),
|
||||||
aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY],
|
aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY],
|
||||||
aws_access_key_id=data[CONF_ACCESS_KEY_ID],
|
aws_access_key_id=data[CONF_ACCESS_KEY_ID],
|
||||||
|
config=config,
|
||||||
).__aenter__()
|
).__aenter__()
|
||||||
await client.head_bucket(Bucket=data[CONF_BUCKET])
|
await client.head_bucket(Bucket=data[CONF_BUCKET])
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
|
@ -46,6 +46,7 @@ from homeassistant.const import (
|
|||||||
CONF_TOKEN,
|
CONF_TOKEN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_component
|
from homeassistant.helpers import entity_component
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
@ -53,6 +54,7 @@ from homeassistant.util import dt as dt_util
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_SESSION_ID,
|
CONF_SESSION_ID,
|
||||||
|
DOMAIN,
|
||||||
ENCRYPTED_WEBSOCKET_PORT,
|
ENCRYPTED_WEBSOCKET_PORT,
|
||||||
LEGACY_PORT,
|
LEGACY_PORT,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
@ -371,9 +373,13 @@ class SamsungTVLegacyBridge(SamsungTVBridge):
|
|||||||
except (ConnectionClosed, BrokenPipeError):
|
except (ConnectionClosed, BrokenPipeError):
|
||||||
# BrokenPipe can occur when the commands is sent to fast
|
# BrokenPipe can occur when the commands is sent to fast
|
||||||
self._remote = None
|
self._remote = None
|
||||||
except (UnhandledResponse, AccessDenied):
|
except (UnhandledResponse, AccessDenied) as err:
|
||||||
# We got a response so it's on.
|
# We got a response so it's on.
|
||||||
LOGGER.debug("Failed sending command %s", key, exc_info=True)
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="error_sending_command",
|
||||||
|
translation_placeholders={"error": repr(err), "host": self.host},
|
||||||
|
) from err
|
||||||
except OSError:
|
except OSError:
|
||||||
# Different reasons, e.g. hostname not resolveable
|
# Different reasons, e.g. hostname not resolveable
|
||||||
pass
|
pass
|
||||||
|
@ -29,13 +29,14 @@ from homeassistant.components.media_player import (
|
|||||||
MediaType,
|
MediaType,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util.async_ import create_eager_task
|
from homeassistant.util.async_ import create_eager_task
|
||||||
|
|
||||||
from .bridge import SamsungTVWSBridge
|
from .bridge import SamsungTVWSBridge
|
||||||
from .const import CONF_SSDP_RENDERING_CONTROL_LOCATION, LOGGER
|
from .const import CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN, LOGGER
|
||||||
from .coordinator import SamsungTVConfigEntry, SamsungTVDataUpdateCoordinator
|
from .coordinator import SamsungTVConfigEntry, SamsungTVDataUpdateCoordinator
|
||||||
from .entity import SamsungTVEntity
|
from .entity import SamsungTVEntity
|
||||||
|
|
||||||
@ -308,7 +309,12 @@ class SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity):
|
|||||||
try:
|
try:
|
||||||
await dmr_device.async_set_volume_level(volume)
|
await dmr_device.async_set_volume_level(volume)
|
||||||
except UpnpActionResponseError as err:
|
except UpnpActionResponseError as err:
|
||||||
LOGGER.warning("Unable to set volume level on %s: %r", self._host, err)
|
assert self._host
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="error_set_volume",
|
||||||
|
translation_placeholders={"error": repr(err), "host": self._host},
|
||||||
|
) from err
|
||||||
|
|
||||||
async def async_volume_up(self) -> None:
|
async def async_volume_up(self) -> None:
|
||||||
"""Volume up the media player."""
|
"""Volume up the media player."""
|
||||||
|
@ -68,6 +68,12 @@
|
|||||||
"service_unsupported": {
|
"service_unsupported": {
|
||||||
"message": "Entity {entity} does not support this action."
|
"message": "Entity {entity} does not support this action."
|
||||||
},
|
},
|
||||||
|
"error_set_volume": {
|
||||||
|
"message": "Unable to set volume level on {host}: {error}"
|
||||||
|
},
|
||||||
|
"error_sending_command": {
|
||||||
|
"message": "Unable to send command to {host}: {error}"
|
||||||
|
},
|
||||||
"encrypted_mode_auth_failed": {
|
"encrypted_mode_auth_failed": {
|
||||||
"message": "Token and session ID are required in encrypted mode."
|
"message": "Token and session ID are required in encrypted mode."
|
||||||
},
|
},
|
||||||
|
@ -42,7 +42,7 @@ rules:
|
|||||||
diagnostics: done
|
diagnostics: done
|
||||||
discovery-update-info: done
|
discovery-update-info: done
|
||||||
discovery: done
|
discovery: done
|
||||||
docs-data-update: todo
|
docs-data-update: done
|
||||||
docs-examples: done
|
docs-examples: done
|
||||||
docs-known-limitations: done
|
docs-known-limitations: done
|
||||||
docs-supported-devices: done
|
docs-supported-devices: done
|
||||||
@ -56,8 +56,8 @@ rules:
|
|||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
entity-translations: todo
|
entity-translations: todo
|
||||||
exception-translations: todo
|
exception-translations: done
|
||||||
icon-translations: todo
|
icon-translations: done
|
||||||
reconfiguration-flow: done
|
reconfiguration-flow: done
|
||||||
repair-issues: done
|
repair-issues: done
|
||||||
stale-devices:
|
stale-devices:
|
||||||
|
@ -29,17 +29,17 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "SMS Verification",
|
"title": "SMS verification",
|
||||||
"description": "Enter your phone number (same as what you used to register to the tami4 app)",
|
"description": "Enter your phone number (same as what you used to register to the tami4 app)",
|
||||||
"data": {
|
"data": {
|
||||||
"phone": "Phone Number"
|
"phone": "Phone number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
"title": "[%key:component::tami4::config::step::user::title%]",
|
"title": "[%key:component::tami4::config::step::user::title%]",
|
||||||
"description": "Enter the code you received via SMS",
|
"description": "Enter the code you received via SMS",
|
||||||
"data": {
|
"data": {
|
||||||
"otp": "SMS Code"
|
"otp": "SMS code"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -76,10 +76,10 @@
|
|||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
"auto_charge": {
|
"auto_charge": {
|
||||||
"name": "Auto charge"
|
"name": "Auto-charge"
|
||||||
},
|
},
|
||||||
"session_active": {
|
"session_active": {
|
||||||
"name": "Charging Enabled"
|
"name": "Charging enabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -23,7 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER, MODELS
|
from .const import DOMAIN, LOGGER
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
TeslemetryEnergyHistoryCoordinator,
|
TeslemetryEnergyHistoryCoordinator,
|
||||||
TeslemetryEnergySiteInfoCoordinator,
|
TeslemetryEnergySiteInfoCoordinator,
|
||||||
@ -119,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
|
|||||||
manufacturer="Tesla",
|
manufacturer="Tesla",
|
||||||
configuration_url="https://teslemetry.com/console",
|
configuration_url="https://teslemetry.com/console",
|
||||||
name=product["display_name"],
|
name=product["display_name"],
|
||||||
model=MODELS.get(vin[3]),
|
model=api.model,
|
||||||
serial_number=vin,
|
serial_number=vin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ from .const import TeslemetryState
|
|||||||
from .entity import (
|
from .entity import (
|
||||||
TeslemetryEnergyInfoEntity,
|
TeslemetryEnergyInfoEntity,
|
||||||
TeslemetryEnergyLiveEntity,
|
TeslemetryEnergyLiveEntity,
|
||||||
TeslemetryVehicleEntity,
|
TeslemetryVehiclePollingEntity,
|
||||||
TeslemetryVehicleStreamEntity,
|
TeslemetryVehicleStreamEntity,
|
||||||
)
|
)
|
||||||
from .models import TeslemetryEnergyData, TeslemetryVehicleData
|
from .models import TeslemetryEnergyData, TeslemetryVehicleData
|
||||||
@ -58,26 +58,28 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="state",
|
key="state",
|
||||||
polling=True,
|
polling=True,
|
||||||
polling_value_fn=lambda x: x == TeslemetryState.ONLINE,
|
polling_value_fn=lambda value: value == TeslemetryState.ONLINE,
|
||||||
streaming_listener=lambda x, y: x.listen_State(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_State(callback),
|
||||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="cellular",
|
key="cellular",
|
||||||
streaming_listener=lambda x, y: x.listen_Cellular(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_Cellular(callback),
|
||||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="wifi",
|
key="wifi",
|
||||||
streaming_listener=lambda x, y: x.listen_Wifi(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_Wifi(callback),
|
||||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="charge_state_battery_heater_on",
|
key="charge_state_battery_heater_on",
|
||||||
polling=True,
|
polling=True,
|
||||||
streaming_listener=lambda x, y: x.listen_BatteryHeaterOn(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_BatteryHeaterOn(
|
||||||
|
callback
|
||||||
|
),
|
||||||
device_class=BinarySensorDeviceClass.HEAT,
|
device_class=BinarySensorDeviceClass.HEAT,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -85,8 +87,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="charge_state_charger_phases",
|
key="charge_state_charger_phases",
|
||||||
polling=True,
|
polling=True,
|
||||||
streaming_listener=lambda x, y: x.listen_ChargerPhases(
|
streaming_listener=lambda vehicle, callback: vehicle.listen_ChargerPhases(
|
||||||
lambda z: y(None if z is None else z > 1)
|
lambda value: callback(None if value is None else value > 1)
|
||||||
),
|
),
|
||||||
polling_value_fn=lambda x: cast(int, x) > 1,
|
polling_value_fn=lambda x: cast(int, x) > 1,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -94,7 +96,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="charge_state_preconditioning_enabled",
|
key="charge_state_preconditioning_enabled",
|
||||||
polling=True,
|
polling=True,
|
||||||
streaming_listener=lambda x, y: x.listen_PreconditioningEnabled(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_PreconditioningEnabled(callback),
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
@ -107,7 +110,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="charge_state_scheduled_charging_pending",
|
key="charge_state_scheduled_charging_pending",
|
||||||
polling=True,
|
polling=True,
|
||||||
streaming_listener=lambda x, y: x.listen_ScheduledChargingPending(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_ScheduledChargingPending(callback),
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
@ -175,8 +179,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="vehicle_state_fd_window",
|
key="vehicle_state_fd_window",
|
||||||
polling=True,
|
polling=True,
|
||||||
streaming_listener=lambda x, y: x.listen_FrontDriverWindow(
|
streaming_listener=lambda vehicle, callback: vehicle.listen_FrontDriverWindow(
|
||||||
lambda z: y(WINDOW_STATES.get(z))
|
lambda value: callback(None if value is None else WINDOW_STATES.get(value))
|
||||||
),
|
),
|
||||||
device_class=BinarySensorDeviceClass.WINDOW,
|
device_class=BinarySensorDeviceClass.WINDOW,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@ -184,8 +188,9 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="vehicle_state_fp_window",
|
key="vehicle_state_fp_window",
|
||||||
polling=True,
|
polling=True,
|
||||||
streaming_listener=lambda x, y: x.listen_FrontPassengerWindow(
|
streaming_listener=lambda vehicle,
|
||||||
lambda z: y(WINDOW_STATES.get(z))
|
callback: vehicle.listen_FrontPassengerWindow(
|
||||||
|
lambda value: callback(None if value is None else WINDOW_STATES.get(value))
|
||||||
),
|
),
|
||||||
device_class=BinarySensorDeviceClass.WINDOW,
|
device_class=BinarySensorDeviceClass.WINDOW,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@ -193,8 +198,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="vehicle_state_rd_window",
|
key="vehicle_state_rd_window",
|
||||||
polling=True,
|
polling=True,
|
||||||
streaming_listener=lambda x, y: x.listen_RearDriverWindow(
|
streaming_listener=lambda vehicle, callback: vehicle.listen_RearDriverWindow(
|
||||||
lambda z: y(WINDOW_STATES.get(z))
|
lambda value: callback(None if value is None else WINDOW_STATES.get(value))
|
||||||
),
|
),
|
||||||
device_class=BinarySensorDeviceClass.WINDOW,
|
device_class=BinarySensorDeviceClass.WINDOW,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@ -202,8 +207,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="vehicle_state_rp_window",
|
key="vehicle_state_rp_window",
|
||||||
polling=True,
|
polling=True,
|
||||||
streaming_listener=lambda x, y: x.listen_RearPassengerWindow(
|
streaming_listener=lambda vehicle, callback: vehicle.listen_RearPassengerWindow(
|
||||||
lambda z: y(WINDOW_STATES.get(z))
|
lambda value: callback(None if value is None else WINDOW_STATES.get(value))
|
||||||
),
|
),
|
||||||
device_class=BinarySensorDeviceClass.WINDOW,
|
device_class=BinarySensorDeviceClass.WINDOW,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@ -212,217 +217,287 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
key="vehicle_state_df",
|
key="vehicle_state_df",
|
||||||
polling=True,
|
polling=True,
|
||||||
device_class=BinarySensorDeviceClass.DOOR,
|
device_class=BinarySensorDeviceClass.DOOR,
|
||||||
streaming_listener=lambda x, y: x.listen_FrontDriverDoor(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_FrontDriverDoor(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="vehicle_state_dr",
|
key="vehicle_state_dr",
|
||||||
polling=True,
|
polling=True,
|
||||||
device_class=BinarySensorDeviceClass.DOOR,
|
device_class=BinarySensorDeviceClass.DOOR,
|
||||||
streaming_listener=lambda x, y: x.listen_RearDriverDoor(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_RearDriverDoor(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="vehicle_state_pf",
|
key="vehicle_state_pf",
|
||||||
polling=True,
|
polling=True,
|
||||||
device_class=BinarySensorDeviceClass.DOOR,
|
device_class=BinarySensorDeviceClass.DOOR,
|
||||||
streaming_listener=lambda x, y: x.listen_FrontPassengerDoor(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_FrontPassengerDoor(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="vehicle_state_pr",
|
key="vehicle_state_pr",
|
||||||
polling=True,
|
polling=True,
|
||||||
device_class=BinarySensorDeviceClass.DOOR,
|
device_class=BinarySensorDeviceClass.DOOR,
|
||||||
streaming_listener=lambda x, y: x.listen_RearPassengerDoor(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_RearPassengerDoor(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="automatic_blind_spot_camera",
|
key="automatic_blind_spot_camera",
|
||||||
streaming_listener=lambda x, y: x.listen_AutomaticBlindSpotCamera(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_AutomaticBlindSpotCamera(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="automatic_emergency_braking_off",
|
key="automatic_emergency_braking_off",
|
||||||
streaming_listener=lambda x, y: x.listen_AutomaticEmergencyBrakingOff(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_AutomaticEmergencyBrakingOff(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="blind_spot_collision_warning_chime",
|
key="blind_spot_collision_warning_chime",
|
||||||
streaming_listener=lambda x, y: x.listen_BlindSpotCollisionWarningChime(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_BlindSpotCollisionWarningChime(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="bms_full_charge_complete",
|
key="bms_full_charge_complete",
|
||||||
streaming_listener=lambda x, y: x.listen_BmsFullchargecomplete(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_BmsFullchargecomplete(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="brake_pedal",
|
key="brake_pedal",
|
||||||
streaming_listener=lambda x, y: x.listen_BrakePedal(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_BrakePedal(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="charge_port_cold_weather_mode",
|
key="charge_port_cold_weather_mode",
|
||||||
streaming_listener=lambda x, y: x.listen_ChargePortColdWeatherMode(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_ChargePortColdWeatherMode(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="service_mode",
|
key="service_mode",
|
||||||
streaming_listener=lambda x, y: x.listen_ServiceMode(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_ServiceMode(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="pin_to_drive_enabled",
|
key="pin_to_drive_enabled",
|
||||||
streaming_listener=lambda x, y: x.listen_PinToDriveEnabled(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_PinToDriveEnabled(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="drive_rail",
|
key="drive_rail",
|
||||||
streaming_listener=lambda x, y: x.listen_DriveRail(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_DriveRail(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="driver_seat_belt",
|
key="driver_seat_belt",
|
||||||
streaming_listener=lambda x, y: x.listen_DriverSeatBelt(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_DriverSeatBelt(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="driver_seat_occupied",
|
key="driver_seat_occupied",
|
||||||
streaming_listener=lambda x, y: x.listen_DriverSeatOccupied(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_DriverSeatOccupied(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="passenger_seat_belt",
|
key="passenger_seat_belt",
|
||||||
streaming_listener=lambda x, y: x.listen_PassengerSeatBelt(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_PassengerSeatBelt(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="fast_charger_present",
|
key="fast_charger_present",
|
||||||
streaming_listener=lambda x, y: x.listen_FastChargerPresent(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_FastChargerPresent(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="gps_state",
|
key="gps_state",
|
||||||
streaming_listener=lambda x, y: x.listen_GpsState(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_GpsState(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="guest_mode_enabled",
|
key="guest_mode_enabled",
|
||||||
streaming_listener=lambda x, y: x.listen_GuestModeEnabled(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_GuestModeEnabled(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="dc_dc_enable",
|
key="dc_dc_enable",
|
||||||
streaming_listener=lambda x, y: x.listen_DCDCEnable(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_DCDCEnable(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="emergency_lane_departure_avoidance",
|
key="emergency_lane_departure_avoidance",
|
||||||
streaming_listener=lambda x, y: x.listen_EmergencyLaneDepartureAvoidance(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_EmergencyLaneDepartureAvoidance(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="supercharger_session_trip_planner",
|
key="supercharger_session_trip_planner",
|
||||||
streaming_listener=lambda x, y: x.listen_SuperchargerSessionTripPlanner(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_SuperchargerSessionTripPlanner(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="wiper_heat_enabled",
|
key="wiper_heat_enabled",
|
||||||
streaming_listener=lambda x, y: x.listen_WiperHeatEnabled(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_WiperHeatEnabled(
|
||||||
|
callback
|
||||||
|
),
|
||||||
streaming_firmware="2024.44.25",
|
streaming_firmware="2024.44.25",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="rear_display_hvac_enabled",
|
key="rear_display_hvac_enabled",
|
||||||
streaming_listener=lambda x, y: x.listen_RearDisplayHvacEnabled(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_RearDisplayHvacEnabled(callback),
|
||||||
streaming_firmware="2024.44.25",
|
streaming_firmware="2024.44.25",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="offroad_lightbar_present",
|
key="offroad_lightbar_present",
|
||||||
streaming_listener=lambda x, y: x.listen_OffroadLightbarPresent(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_OffroadLightbarPresent(callback),
|
||||||
streaming_firmware="2024.44.25",
|
streaming_firmware="2024.44.25",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="homelink_nearby",
|
key="homelink_nearby",
|
||||||
streaming_listener=lambda x, y: x.listen_HomelinkNearby(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_HomelinkNearby(
|
||||||
|
callback
|
||||||
|
),
|
||||||
streaming_firmware="2024.44.25",
|
streaming_firmware="2024.44.25",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="europe_vehicle",
|
key="europe_vehicle",
|
||||||
streaming_listener=lambda x, y: x.listen_EuropeVehicle(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_EuropeVehicle(
|
||||||
|
callback
|
||||||
|
),
|
||||||
streaming_firmware="2024.44.25",
|
streaming_firmware="2024.44.25",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="right_hand_drive",
|
key="right_hand_drive",
|
||||||
streaming_listener=lambda x, y: x.listen_RightHandDrive(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_RightHandDrive(
|
||||||
|
callback
|
||||||
|
),
|
||||||
streaming_firmware="2024.44.25",
|
streaming_firmware="2024.44.25",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="located_at_home",
|
key="located_at_home",
|
||||||
streaming_listener=lambda x, y: x.listen_LocatedAtHome(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_LocatedAtHome(
|
||||||
|
callback
|
||||||
|
),
|
||||||
streaming_firmware="2024.44.32",
|
streaming_firmware="2024.44.32",
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="located_at_work",
|
key="located_at_work",
|
||||||
streaming_listener=lambda x, y: x.listen_LocatedAtWork(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_LocatedAtWork(
|
||||||
|
callback
|
||||||
|
),
|
||||||
streaming_firmware="2024.44.32",
|
streaming_firmware="2024.44.32",
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="located_at_favorite",
|
key="located_at_favorite",
|
||||||
streaming_listener=lambda x, y: x.listen_LocatedAtFavorite(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_LocatedAtFavorite(
|
||||||
|
callback
|
||||||
|
),
|
||||||
streaming_firmware="2024.44.32",
|
streaming_firmware="2024.44.32",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="charge_enable_request",
|
key="charge_enable_request",
|
||||||
streaming_listener=lambda x, y: x.listen_ChargeEnableRequest(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_ChargeEnableRequest(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="defrost_for_preconditioning",
|
key="defrost_for_preconditioning",
|
||||||
streaming_listener=lambda x, y: x.listen_DefrostForPreconditioning(y),
|
streaming_listener=lambda vehicle,
|
||||||
|
callback: vehicle.listen_DefrostForPreconditioning(callback),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
streaming_firmware="2024.44.25",
|
streaming_firmware="2024.44.25",
|
||||||
),
|
),
|
||||||
|
TeslemetryBinarySensorEntityDescription(
|
||||||
|
key="lights_hazards_active",
|
||||||
|
streaming_listener=lambda x, y: x.listen_LightsHazardsActive(y),
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
streaming_firmware="2025.2.6",
|
||||||
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="lights_high_beams",
|
key="lights_high_beams",
|
||||||
streaming_listener=lambda x, y: x.listen_LightsHighBeams(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_LightsHighBeams(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
streaming_firmware="2025.2.6",
|
streaming_firmware="2025.2.6",
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="seat_vent_enabled",
|
key="seat_vent_enabled",
|
||||||
streaming_listener=lambda x, y: x.listen_SeatVentEnabled(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_SeatVentEnabled(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
streaming_firmware="2025.2.6",
|
streaming_firmware="2025.2.6",
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="speed_limit_mode",
|
key="speed_limit_mode",
|
||||||
streaming_listener=lambda x, y: x.listen_SpeedLimitMode(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_SpeedLimitMode(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="remote_start_enabled",
|
key="remote_start_enabled",
|
||||||
streaming_listener=lambda x, y: x.listen_RemoteStartEnabled(y),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_RemoteStartEnabled(
|
||||||
|
callback
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="hvil",
|
key="hvil",
|
||||||
streaming_listener=lambda x, y: x.listen_Hvil(lambda z: y(z == "Fault")),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_Hvil(
|
||||||
|
lambda value: callback(None if value is None else value == "Fault")
|
||||||
|
),
|
||||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="hvac_auto_mode",
|
key="hvac_auto_mode",
|
||||||
streaming_listener=lambda x, y: x.listen_HvacAutoMode(lambda z: y(z == "On")),
|
streaming_listener=lambda vehicle, callback: vehicle.listen_HvacAutoMode(
|
||||||
|
lambda value: callback(None if value is None else value == "On")
|
||||||
|
),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -431,7 +506,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
|||||||
ENERGY_LIVE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
ENERGY_LIVE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||||
TeslemetryBinarySensorEntityDescription(
|
TeslemetryBinarySensorEntityDescription(
|
||||||
key="grid_status",
|
key="grid_status",
|
||||||
polling_value_fn=lambda x: x == "Active",
|
polling_value_fn=lambda value: value == "Active",
|
||||||
device_class=BinarySensorDeviceClass.POWER,
|
device_class=BinarySensorDeviceClass.POWER,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
@ -494,7 +569,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
|
|
||||||
class TeslemetryVehiclePollingBinarySensorEntity(
|
class TeslemetryVehiclePollingBinarySensorEntity(
|
||||||
TeslemetryVehicleEntity, BinarySensorEntity
|
TeslemetryVehiclePollingEntity, BinarySensorEntity
|
||||||
):
|
):
|
||||||
"""Base class for Teslemetry vehicle binary sensors."""
|
"""Base class for Teslemetry vehicle binary sensors."""
|
||||||
|
|
||||||
|
@ -7,13 +7,14 @@ from dataclasses import dataclass
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from tesla_fleet_api.const import Scope
|
from tesla_fleet_api.const import Scope
|
||||||
|
from tesla_fleet_api.teslemetry import Vehicle
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import TeslemetryConfigEntry
|
from . import TeslemetryConfigEntry
|
||||||
from .entity import TeslemetryVehicleEntity
|
from .entity import TeslemetryVehiclePollingEntity
|
||||||
from .helpers import handle_command, handle_vehicle_command
|
from .helpers import handle_command, handle_vehicle_command
|
||||||
from .models import TeslemetryVehicleData
|
from .models import TeslemetryVehicleData
|
||||||
|
|
||||||
@ -73,9 +74,10 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryButtonEntity(TeslemetryVehicleEntity, ButtonEntity):
|
class TeslemetryButtonEntity(TeslemetryVehiclePollingEntity, ButtonEntity):
|
||||||
"""Base class for Teslemetry buttons."""
|
"""Base class for Teslemetry buttons."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
entity_description: TeslemetryButtonEntityDescription
|
entity_description: TeslemetryButtonEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -30,7 +30,7 @@ from . import TeslemetryConfigEntry
|
|||||||
from .const import DOMAIN, TeslemetryClimateSide
|
from .const import DOMAIN, TeslemetryClimateSide
|
||||||
from .entity import (
|
from .entity import (
|
||||||
TeslemetryRootEntity,
|
TeslemetryRootEntity,
|
||||||
TeslemetryVehicleEntity,
|
TeslemetryVehiclePollingEntity,
|
||||||
TeslemetryVehicleStreamEntity,
|
TeslemetryVehicleStreamEntity,
|
||||||
)
|
)
|
||||||
from .helpers import handle_vehicle_command
|
from .helpers import handle_vehicle_command
|
||||||
@ -64,7 +64,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
chain(
|
chain(
|
||||||
(
|
(
|
||||||
TeslemetryPollingClimateEntity(
|
TeslemetryVehiclePollingClimateEntity(
|
||||||
vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes
|
vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes
|
||||||
)
|
)
|
||||||
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
|
||||||
@ -74,7 +74,7 @@ async def async_setup_entry(
|
|||||||
for vehicle in entry.runtime_data.vehicles
|
for vehicle in entry.runtime_data.vehicles
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TeslemetryPollingCabinOverheatProtectionEntity(
|
TeslemetryVehiclePollingCabinOverheatProtectionEntity(
|
||||||
vehicle, entry.runtime_data.scopes
|
vehicle, entry.runtime_data.scopes
|
||||||
)
|
)
|
||||||
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
|
||||||
@ -91,7 +91,6 @@ class TeslemetryClimateEntity(TeslemetryRootEntity, ClimateEntity):
|
|||||||
"""Vehicle Climate Control."""
|
"""Vehicle Climate Control."""
|
||||||
|
|
||||||
api: Vehicle
|
api: Vehicle
|
||||||
|
|
||||||
_attr_precision = PRECISION_HALVES
|
_attr_precision = PRECISION_HALVES
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
|
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
|
||||||
@ -178,7 +177,9 @@ class TeslemetryClimateEntity(TeslemetryRootEntity, ClimateEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
|
class TeslemetryVehiclePollingClimateEntity(
|
||||||
|
TeslemetryClimateEntity, TeslemetryVehiclePollingEntity
|
||||||
|
):
|
||||||
"""Polling vehicle climate entity."""
|
"""Polling vehicle climate entity."""
|
||||||
|
|
||||||
_attr_supported_features = (
|
_attr_supported_features = (
|
||||||
@ -370,7 +371,6 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryRootEntity, ClimateEntit
|
|||||||
"""Vehicle Cabin Overheat Protection."""
|
"""Vehicle Cabin Overheat Protection."""
|
||||||
|
|
||||||
api: Vehicle
|
api: Vehicle
|
||||||
|
|
||||||
_attr_precision = PRECISION_WHOLE
|
_attr_precision = PRECISION_WHOLE
|
||||||
_attr_target_temperature_step = 5
|
_attr_target_temperature_step = 5
|
||||||
_attr_min_temp = 30
|
_attr_min_temp = 30
|
||||||
@ -430,8 +430,8 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryRootEntity, ClimateEntit
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingCabinOverheatProtectionEntity(
|
class TeslemetryVehiclePollingCabinOverheatProtectionEntity(
|
||||||
TeslemetryVehicleEntity, TeslemetryCabinOverheatProtectionEntity
|
TeslemetryVehiclePollingEntity, TeslemetryCabinOverheatProtectionEntity
|
||||||
):
|
):
|
||||||
"""Vehicle Cabin Overheat Protection."""
|
"""Vehicle Cabin Overheat Protection."""
|
||||||
|
|
||||||
|
@ -9,13 +9,6 @@ DOMAIN = "teslemetry"
|
|||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
MODELS = {
|
|
||||||
"S": "Model S",
|
|
||||||
"3": "Model 3",
|
|
||||||
"X": "Model X",
|
|
||||||
"Y": "Model Y",
|
|
||||||
}
|
|
||||||
|
|
||||||
ENERGY_HISTORY_FIELDS = [
|
ENERGY_HISTORY_FIELDS = [
|
||||||
"solar_energy_exported",
|
"solar_energy_exported",
|
||||||
"generator_energy_exported",
|
"generator_energy_exported",
|
||||||
|
@ -6,6 +6,7 @@ from itertools import chain
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from tesla_fleet_api.const import Scope, SunRoofCommand, Trunk, WindowCommand
|
from tesla_fleet_api.const import Scope, SunRoofCommand, Trunk, WindowCommand
|
||||||
|
from tesla_fleet_api.teslemetry import Vehicle
|
||||||
from teslemetry_stream import Signal
|
from teslemetry_stream import Signal
|
||||||
from teslemetry_stream.const import WindowState
|
from teslemetry_stream.const import WindowState
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
|||||||
from . import TeslemetryConfigEntry
|
from . import TeslemetryConfigEntry
|
||||||
from .entity import (
|
from .entity import (
|
||||||
TeslemetryRootEntity,
|
TeslemetryRootEntity,
|
||||||
TeslemetryVehicleEntity,
|
TeslemetryVehiclePollingEntity,
|
||||||
TeslemetryVehicleStreamEntity,
|
TeslemetryVehicleStreamEntity,
|
||||||
)
|
)
|
||||||
from .helpers import handle_vehicle_command
|
from .helpers import handle_vehicle_command
|
||||||
@ -43,13 +44,15 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
chain(
|
chain(
|
||||||
(
|
(
|
||||||
TeslemetryPollingWindowEntity(vehicle, entry.runtime_data.scopes)
|
TeslemetryVehiclePollingWindowEntity(vehicle, entry.runtime_data.scopes)
|
||||||
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
||||||
else TeslemetryStreamingWindowEntity(vehicle, entry.runtime_data.scopes)
|
else TeslemetryStreamingWindowEntity(vehicle, entry.runtime_data.scopes)
|
||||||
for vehicle in entry.runtime_data.vehicles
|
for vehicle in entry.runtime_data.vehicles
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TeslemetryPollingChargePortEntity(vehicle, entry.runtime_data.scopes)
|
TeslemetryVehiclePollingChargePortEntity(
|
||||||
|
vehicle, entry.runtime_data.scopes
|
||||||
|
)
|
||||||
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
|
||||||
else TeslemetryStreamingChargePortEntity(
|
else TeslemetryStreamingChargePortEntity(
|
||||||
vehicle, entry.runtime_data.scopes
|
vehicle, entry.runtime_data.scopes
|
||||||
@ -57,7 +60,9 @@ async def async_setup_entry(
|
|||||||
for vehicle in entry.runtime_data.vehicles
|
for vehicle in entry.runtime_data.vehicles
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TeslemetryPollingFrontTrunkEntity(vehicle, entry.runtime_data.scopes)
|
TeslemetryVehiclePollingFrontTrunkEntity(
|
||||||
|
vehicle, entry.runtime_data.scopes
|
||||||
|
)
|
||||||
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
||||||
else TeslemetryStreamingFrontTrunkEntity(
|
else TeslemetryStreamingFrontTrunkEntity(
|
||||||
vehicle, entry.runtime_data.scopes
|
vehicle, entry.runtime_data.scopes
|
||||||
@ -65,7 +70,9 @@ async def async_setup_entry(
|
|||||||
for vehicle in entry.runtime_data.vehicles
|
for vehicle in entry.runtime_data.vehicles
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TeslemetryPollingRearTrunkEntity(vehicle, entry.runtime_data.scopes)
|
TeslemetryVehiclePollingRearTrunkEntity(
|
||||||
|
vehicle, entry.runtime_data.scopes
|
||||||
|
)
|
||||||
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
||||||
else TeslemetryStreamingRearTrunkEntity(
|
else TeslemetryStreamingRearTrunkEntity(
|
||||||
vehicle, entry.runtime_data.scopes
|
vehicle, entry.runtime_data.scopes
|
||||||
@ -97,6 +104,7 @@ class CoverRestoreEntity(RestoreEntity, CoverEntity):
|
|||||||
class TeslemetryWindowEntity(TeslemetryRootEntity, CoverEntity):
|
class TeslemetryWindowEntity(TeslemetryRootEntity, CoverEntity):
|
||||||
"""Base class for window cover entities."""
|
"""Base class for window cover entities."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
_attr_device_class = CoverDeviceClass.WINDOW
|
_attr_device_class = CoverDeviceClass.WINDOW
|
||||||
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||||
|
|
||||||
@ -121,8 +129,8 @@ class TeslemetryWindowEntity(TeslemetryRootEntity, CoverEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingWindowEntity(
|
class TeslemetryVehiclePollingWindowEntity(
|
||||||
TeslemetryVehicleEntity, TeslemetryWindowEntity, CoverEntity
|
TeslemetryVehiclePollingEntity, TeslemetryWindowEntity, CoverEntity
|
||||||
):
|
):
|
||||||
"""Polling cover entity for windows."""
|
"""Polling cover entity for windows."""
|
||||||
|
|
||||||
@ -218,6 +226,7 @@ class TeslemetryChargePortEntity(
|
|||||||
):
|
):
|
||||||
"""Base class for for charge port cover entities."""
|
"""Base class for for charge port cover entities."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
_attr_device_class = CoverDeviceClass.DOOR
|
_attr_device_class = CoverDeviceClass.DOOR
|
||||||
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||||
|
|
||||||
@ -238,8 +247,8 @@ class TeslemetryChargePortEntity(
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingChargePortEntity(
|
class TeslemetryVehiclePollingChargePortEntity(
|
||||||
TeslemetryVehicleEntity, TeslemetryChargePortEntity
|
TeslemetryVehiclePollingEntity, TeslemetryChargePortEntity
|
||||||
):
|
):
|
||||||
"""Polling cover entity for the charge port."""
|
"""Polling cover entity for the charge port."""
|
||||||
|
|
||||||
@ -298,6 +307,7 @@ class TeslemetryStreamingChargePortEntity(
|
|||||||
class TeslemetryFrontTrunkEntity(TeslemetryRootEntity, CoverEntity):
|
class TeslemetryFrontTrunkEntity(TeslemetryRootEntity, CoverEntity):
|
||||||
"""Base class for the front trunk cover entities."""
|
"""Base class for the front trunk cover entities."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
_attr_device_class = CoverDeviceClass.DOOR
|
_attr_device_class = CoverDeviceClass.DOOR
|
||||||
_attr_supported_features = CoverEntityFeature.OPEN
|
_attr_supported_features = CoverEntityFeature.OPEN
|
||||||
|
|
||||||
@ -312,8 +322,8 @@ class TeslemetryFrontTrunkEntity(TeslemetryRootEntity, CoverEntity):
|
|||||||
# In the future this could be extended to add aftermarket close support through a option flow
|
# In the future this could be extended to add aftermarket close support through a option flow
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingFrontTrunkEntity(
|
class TeslemetryVehiclePollingFrontTrunkEntity(
|
||||||
TeslemetryVehicleEntity, TeslemetryFrontTrunkEntity
|
TeslemetryVehiclePollingEntity, TeslemetryFrontTrunkEntity
|
||||||
):
|
):
|
||||||
"""Polling cover entity for the front trunk."""
|
"""Polling cover entity for the front trunk."""
|
||||||
|
|
||||||
@ -359,6 +369,7 @@ class TeslemetryStreamingFrontTrunkEntity(
|
|||||||
class TeslemetryRearTrunkEntity(TeslemetryRootEntity, CoverEntity):
|
class TeslemetryRearTrunkEntity(TeslemetryRootEntity, CoverEntity):
|
||||||
"""Cover entity for the rear trunk."""
|
"""Cover entity for the rear trunk."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
_attr_device_class = CoverDeviceClass.DOOR
|
_attr_device_class = CoverDeviceClass.DOOR
|
||||||
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||||
|
|
||||||
@ -381,8 +392,8 @@ class TeslemetryRearTrunkEntity(TeslemetryRootEntity, CoverEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingRearTrunkEntity(
|
class TeslemetryVehiclePollingRearTrunkEntity(
|
||||||
TeslemetryVehicleEntity, TeslemetryRearTrunkEntity
|
TeslemetryVehiclePollingEntity, TeslemetryRearTrunkEntity
|
||||||
):
|
):
|
||||||
"""Base class for the rear trunk cover entities."""
|
"""Base class for the rear trunk cover entities."""
|
||||||
|
|
||||||
@ -424,9 +435,10 @@ class TeslemetryStreamingRearTrunkEntity(
|
|||||||
self._attr_is_closed = None if value is None else not value
|
self._attr_is_closed = None if value is None else not value
|
||||||
|
|
||||||
|
|
||||||
class TeslemetrySunroofEntity(TeslemetryVehicleEntity, CoverEntity):
|
class TeslemetrySunroofEntity(TeslemetryVehiclePollingEntity, CoverEntity):
|
||||||
"""Cover entity for the sunroof."""
|
"""Cover entity for the sunroof."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
_attr_device_class = CoverDeviceClass.WINDOW
|
_attr_device_class = CoverDeviceClass.WINDOW
|
||||||
_attr_supported_features = (
|
_attr_supported_features = (
|
||||||
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
|
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
|
||||||
from . import TeslemetryConfigEntry
|
from . import TeslemetryConfigEntry
|
||||||
from .entity import TeslemetryVehicleEntity, TeslemetryVehicleStreamEntity
|
from .entity import TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity
|
||||||
from .models import TeslemetryVehicleData
|
from .models import TeslemetryVehicleData
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@ -74,7 +74,8 @@ async def async_setup_entry(
|
|||||||
"""Set up the Teslemetry device tracker platform from a config entry."""
|
"""Set up the Teslemetry device tracker platform from a config entry."""
|
||||||
|
|
||||||
entities: list[
|
entities: list[
|
||||||
TeslemetryPollingDeviceTrackerEntity | TeslemetryStreamingDeviceTrackerEntity
|
TeslemetryVehiclePollingDeviceTrackerEntity
|
||||||
|
| TeslemetryStreamingDeviceTrackerEntity
|
||||||
] = []
|
] = []
|
||||||
# Only add vehicle location entities if the user has granted vehicle location scope.
|
# Only add vehicle location entities if the user has granted vehicle location scope.
|
||||||
if Scope.VEHICLE_LOCATION not in entry.runtime_data.scopes:
|
if Scope.VEHICLE_LOCATION not in entry.runtime_data.scopes:
|
||||||
@ -85,7 +86,9 @@ async def async_setup_entry(
|
|||||||
if vehicle.api.pre2021 or vehicle.firmware < description.streaming_firmware:
|
if vehicle.api.pre2021 or vehicle.firmware < description.streaming_firmware:
|
||||||
if description.polling_prefix:
|
if description.polling_prefix:
|
||||||
entities.append(
|
entities.append(
|
||||||
TeslemetryPollingDeviceTrackerEntity(vehicle, description)
|
TeslemetryVehiclePollingDeviceTrackerEntity(
|
||||||
|
vehicle, description
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
entities.append(
|
entities.append(
|
||||||
@ -95,7 +98,9 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingDeviceTrackerEntity(TeslemetryVehicleEntity, TrackerEntity):
|
class TeslemetryVehiclePollingDeviceTrackerEntity(
|
||||||
|
TeslemetryVehiclePollingEntity, TrackerEntity
|
||||||
|
):
|
||||||
"""Base class for Teslemetry Tracker Entities."""
|
"""Base class for Teslemetry Tracker Entities."""
|
||||||
|
|
||||||
entity_description: TeslemetryDeviceTrackerEntityDescription
|
entity_description: TeslemetryDeviceTrackerEntityDescription
|
||||||
|
@ -26,7 +26,6 @@ class TeslemetryRootEntity(Entity):
|
|||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
scoped: bool
|
scoped: bool
|
||||||
api: Vehicle | EnergySite
|
|
||||||
|
|
||||||
def raise_for_scope(self, scope: Scope):
|
def raise_for_scope(self, scope: Scope):
|
||||||
"""Raise an error if a scope is not available."""
|
"""Raise an error if a scope is not available."""
|
||||||
@ -38,7 +37,7 @@ class TeslemetryRootEntity(Entity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryEntity(
|
class TeslemetryPollingEntity(
|
||||||
TeslemetryRootEntity,
|
TeslemetryRootEntity,
|
||||||
CoordinatorEntity[
|
CoordinatorEntity[
|
||||||
TeslemetryVehicleDataCoordinator
|
TeslemetryVehicleDataCoordinator
|
||||||
@ -98,7 +97,7 @@ class TeslemetryEntity(
|
|||||||
"""Update the attributes of the entity."""
|
"""Update the attributes of the entity."""
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryVehicleEntity(TeslemetryEntity):
|
class TeslemetryVehiclePollingEntity(TeslemetryPollingEntity):
|
||||||
"""Parent class for Teslemetry Vehicle entities."""
|
"""Parent class for Teslemetry Vehicle entities."""
|
||||||
|
|
||||||
_last_update: int = 0
|
_last_update: int = 0
|
||||||
@ -130,7 +129,7 @@ class TeslemetryVehicleEntity(TeslemetryEntity):
|
|||||||
return self.coordinator.data.get(self.key)
|
return self.coordinator.data.get(self.key)
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryEnergyLiveEntity(TeslemetryEntity):
|
class TeslemetryEnergyLiveEntity(TeslemetryPollingEntity):
|
||||||
"""Parent class for Teslemetry Energy Site Live entities."""
|
"""Parent class for Teslemetry Energy Site Live entities."""
|
||||||
|
|
||||||
api: EnergySite
|
api: EnergySite
|
||||||
@ -151,7 +150,7 @@ class TeslemetryEnergyLiveEntity(TeslemetryEntity):
|
|||||||
super().__init__(data.live_coordinator, key)
|
super().__init__(data.live_coordinator, key)
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryEnergyInfoEntity(TeslemetryEntity):
|
class TeslemetryEnergyInfoEntity(TeslemetryPollingEntity):
|
||||||
"""Parent class for Teslemetry Energy Site Info Entities."""
|
"""Parent class for Teslemetry Energy Site Info Entities."""
|
||||||
|
|
||||||
api: EnergySite
|
api: EnergySite
|
||||||
@ -170,7 +169,7 @@ class TeslemetryEnergyInfoEntity(TeslemetryEntity):
|
|||||||
super().__init__(data.info_coordinator, key)
|
super().__init__(data.info_coordinator, key)
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryEnergyHistoryEntity(TeslemetryEntity):
|
class TeslemetryEnergyHistoryEntity(TeslemetryPollingEntity):
|
||||||
"""Parent class for Teslemetry Energy History Entities."""
|
"""Parent class for Teslemetry Energy History Entities."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -189,7 +188,7 @@ class TeslemetryEnergyHistoryEntity(TeslemetryEntity):
|
|||||||
super().__init__(data.history_coordinator, key)
|
super().__init__(data.history_coordinator, key)
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryWallConnectorEntity(TeslemetryEntity):
|
class TeslemetryWallConnectorEntity(TeslemetryPollingEntity):
|
||||||
"""Parent class for Teslemetry Wall Connector Entities."""
|
"""Parent class for Teslemetry Wall Connector Entities."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
@ -248,6 +247,8 @@ class TeslemetryWallConnectorEntity(TeslemetryEntity):
|
|||||||
class TeslemetryVehicleStreamEntity(TeslemetryRootEntity):
|
class TeslemetryVehicleStreamEntity(TeslemetryRootEntity):
|
||||||
"""Parent class for Teslemetry Vehicle Stream entities."""
|
"""Parent class for Teslemetry Vehicle Stream entities."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
|
|
||||||
def __init__(self, data: TeslemetryVehicleData, key: str) -> None:
|
def __init__(self, data: TeslemetryVehicleData, key: str) -> None:
|
||||||
"""Initialize common aspects of a Teslemetry entity."""
|
"""Initialize common aspects of a Teslemetry entity."""
|
||||||
self.vehicle = data
|
self.vehicle = data
|
||||||
|
@ -73,6 +73,12 @@
|
|||||||
"on": "mdi:snowflake-melt"
|
"on": "mdi:snowflake-melt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lights_hazards_active": {
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:car-light-dimmed",
|
||||||
|
"on": "mdi:hazard-lights"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lights_high_beams": {
|
"lights_high_beams": {
|
||||||
"state": {
|
"state": {
|
||||||
"off": "mdi:car-light-dimmed",
|
"off": "mdi:car-light-dimmed",
|
||||||
|
@ -6,6 +6,7 @@ from itertools import chain
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from tesla_fleet_api.const import Scope
|
from tesla_fleet_api.const import Scope
|
||||||
|
from tesla_fleet_api.teslemetry import Vehicle
|
||||||
|
|
||||||
from homeassistant.components.lock import LockEntity
|
from homeassistant.components.lock import LockEntity
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -17,7 +18,7 @@ from . import TeslemetryConfigEntry
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import (
|
from .entity import (
|
||||||
TeslemetryRootEntity,
|
TeslemetryRootEntity,
|
||||||
TeslemetryVehicleEntity,
|
TeslemetryVehiclePollingEntity,
|
||||||
TeslemetryVehicleStreamEntity,
|
TeslemetryVehicleStreamEntity,
|
||||||
)
|
)
|
||||||
from .helpers import handle_vehicle_command
|
from .helpers import handle_vehicle_command
|
||||||
@ -38,7 +39,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
chain(
|
chain(
|
||||||
(
|
(
|
||||||
TeslemetryPollingVehicleLockEntity(
|
TeslemetryVehiclePollingVehicleLockEntity(
|
||||||
vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes
|
vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes
|
||||||
)
|
)
|
||||||
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
||||||
@ -48,7 +49,7 @@ async def async_setup_entry(
|
|||||||
for vehicle in entry.runtime_data.vehicles
|
for vehicle in entry.runtime_data.vehicles
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TeslemetryPollingCableLockEntity(
|
TeslemetryVehiclePollingCableLockEntity(
|
||||||
vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes
|
vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes
|
||||||
)
|
)
|
||||||
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
if vehicle.api.pre2021 or vehicle.firmware < "2024.26"
|
||||||
@ -64,6 +65,8 @@ async def async_setup_entry(
|
|||||||
class TeslemetryVehicleLockEntity(TeslemetryRootEntity, LockEntity):
|
class TeslemetryVehicleLockEntity(TeslemetryRootEntity, LockEntity):
|
||||||
"""Base vehicle lock entity for Teslemetry."""
|
"""Base vehicle lock entity for Teslemetry."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
|
|
||||||
async def async_lock(self, **kwargs: Any) -> None:
|
async def async_lock(self, **kwargs: Any) -> None:
|
||||||
"""Lock the doors."""
|
"""Lock the doors."""
|
||||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||||
@ -81,8 +84,8 @@ class TeslemetryVehicleLockEntity(TeslemetryRootEntity, LockEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingVehicleLockEntity(
|
class TeslemetryVehiclePollingVehicleLockEntity(
|
||||||
TeslemetryVehicleEntity, TeslemetryVehicleLockEntity
|
TeslemetryVehiclePollingEntity, TeslemetryVehicleLockEntity
|
||||||
):
|
):
|
||||||
"""Polling vehicle lock entity for Teslemetry."""
|
"""Polling vehicle lock entity for Teslemetry."""
|
||||||
|
|
||||||
@ -135,6 +138,8 @@ class TeslemetryStreamingVehicleLockEntity(
|
|||||||
class TeslemetryCableLockEntity(TeslemetryRootEntity, LockEntity):
|
class TeslemetryCableLockEntity(TeslemetryRootEntity, LockEntity):
|
||||||
"""Base cable Lock entity for Teslemetry."""
|
"""Base cable Lock entity for Teslemetry."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
|
|
||||||
async def async_lock(self, **kwargs: Any) -> None:
|
async def async_lock(self, **kwargs: Any) -> None:
|
||||||
"""Charge cable Lock cannot be manually locked."""
|
"""Charge cable Lock cannot be manually locked."""
|
||||||
raise ServiceValidationError(
|
raise ServiceValidationError(
|
||||||
@ -152,8 +157,8 @@ class TeslemetryCableLockEntity(TeslemetryRootEntity, LockEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingCableLockEntity(
|
class TeslemetryVehiclePollingCableLockEntity(
|
||||||
TeslemetryVehicleEntity, TeslemetryCableLockEntity
|
TeslemetryVehiclePollingEntity, TeslemetryCableLockEntity
|
||||||
):
|
):
|
||||||
"""Polling cable lock entity for Teslemetry."""
|
"""Polling cable lock entity for Teslemetry."""
|
||||||
|
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["tesla-fleet-api"],
|
"loggers": ["tesla-fleet-api"],
|
||||||
"requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.5"]
|
"requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.7"]
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
|||||||
from . import TeslemetryConfigEntry
|
from . import TeslemetryConfigEntry
|
||||||
from .entity import (
|
from .entity import (
|
||||||
TeslemetryRootEntity,
|
TeslemetryRootEntity,
|
||||||
TeslemetryVehicleEntity,
|
TeslemetryVehiclePollingEntity,
|
||||||
TeslemetryVehicleStreamEntity,
|
TeslemetryVehicleStreamEntity,
|
||||||
)
|
)
|
||||||
from .helpers import handle_vehicle_command
|
from .helpers import handle_vehicle_command
|
||||||
@ -52,7 +52,7 @@ async def async_setup_entry(
|
|||||||
"""Set up the Teslemetry Media platform from a config entry."""
|
"""Set up the Teslemetry Media platform from a config entry."""
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
TeslemetryPollingMediaEntity(vehicle, entry.runtime_data.scopes)
|
TeslemetryVehiclePollingMediaEntity(vehicle, entry.runtime_data.scopes)
|
||||||
if vehicle.api.pre2021 or vehicle.firmware < "2025.2.6"
|
if vehicle.api.pre2021 or vehicle.firmware < "2025.2.6"
|
||||||
else TeslemetryStreamingMediaEntity(vehicle, entry.runtime_data.scopes)
|
else TeslemetryStreamingMediaEntity(vehicle, entry.runtime_data.scopes)
|
||||||
for vehicle in entry.runtime_data.vehicles
|
for vehicle in entry.runtime_data.vehicles
|
||||||
@ -63,7 +63,6 @@ class TeslemetryMediaEntity(TeslemetryRootEntity, MediaPlayerEntity):
|
|||||||
"""Base vehicle media player class."""
|
"""Base vehicle media player class."""
|
||||||
|
|
||||||
api: Vehicle
|
api: Vehicle
|
||||||
|
|
||||||
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||||
_attr_volume_step = VOLUME_STEP
|
_attr_volume_step = VOLUME_STEP
|
||||||
|
|
||||||
@ -107,7 +106,9 @@ class TeslemetryMediaEntity(TeslemetryRootEntity, MediaPlayerEntity):
|
|||||||
await handle_vehicle_command(self.api.media_prev_track())
|
await handle_vehicle_command(self.api.media_prev_track())
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingMediaEntity(TeslemetryVehicleEntity, TeslemetryMediaEntity):
|
class TeslemetryVehiclePollingMediaEntity(
|
||||||
|
TeslemetryVehiclePollingEntity, TeslemetryMediaEntity
|
||||||
|
):
|
||||||
"""Polling vehicle media player class."""
|
"""Polling vehicle media player class."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -33,7 +33,7 @@ from . import TeslemetryConfigEntry
|
|||||||
from .entity import (
|
from .entity import (
|
||||||
TeslemetryEnergyInfoEntity,
|
TeslemetryEnergyInfoEntity,
|
||||||
TeslemetryRootEntity,
|
TeslemetryRootEntity,
|
||||||
TeslemetryVehicleEntity,
|
TeslemetryVehiclePollingEntity,
|
||||||
TeslemetryVehicleStreamEntity,
|
TeslemetryVehicleStreamEntity,
|
||||||
)
|
)
|
||||||
from .helpers import handle_command, handle_vehicle_command
|
from .helpers import handle_command, handle_vehicle_command
|
||||||
@ -140,7 +140,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
chain(
|
chain(
|
||||||
(
|
(
|
||||||
TeslemetryPollingNumberEntity(
|
TeslemetryVehiclePollingNumberEntity(
|
||||||
vehicle,
|
vehicle,
|
||||||
description,
|
description,
|
||||||
entry.runtime_data.scopes,
|
entry.runtime_data.scopes,
|
||||||
@ -172,6 +172,7 @@ async def async_setup_entry(
|
|||||||
class TeslemetryVehicleNumberEntity(TeslemetryRootEntity, NumberEntity):
|
class TeslemetryVehicleNumberEntity(TeslemetryRootEntity, NumberEntity):
|
||||||
"""Vehicle number entity base class."""
|
"""Vehicle number entity base class."""
|
||||||
|
|
||||||
|
api: Vehicle
|
||||||
entity_description: TeslemetryNumberVehicleEntityDescription
|
entity_description: TeslemetryNumberVehicleEntityDescription
|
||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
@ -183,8 +184,8 @@ class TeslemetryVehicleNumberEntity(TeslemetryRootEntity, NumberEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryPollingNumberEntity(
|
class TeslemetryVehiclePollingNumberEntity(
|
||||||
TeslemetryVehicleEntity, TeslemetryVehicleNumberEntity
|
TeslemetryVehiclePollingEntity, TeslemetryVehicleNumberEntity
|
||||||
):
|
):
|
||||||
"""Vehicle polling number entity."""
|
"""Vehicle polling number entity."""
|
||||||
|
|
||||||
|
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