Compare commits

..

2 Commits

Author SHA1 Message Date
farmio
9d979a1d7f review; mark disabled by default 2025-12-03 20:09:09 +01:00
farmio
0a1214256a Add counter for KNX DataSecure undecodable telegrams 2025-12-03 14:52:04 +01:00
155 changed files with 903 additions and 4614 deletions

View File

@@ -27,6 +27,7 @@
"charliermarsh.ruff",
"ms-python.pylint",
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github",

View File

@@ -416,19 +416,9 @@ jobs:
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do
echo "Copying ${arch} image to DockerHub..."
for attempt in 1 2 3; do
if docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
break
fi
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
sleep 10
if [ "${attempt}" -eq 3 ]; then
echo "Failed after 3 attempts"
exit 1
fi
done
docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done

4
CODEOWNERS generated
View File

@@ -571,8 +571,6 @@ build.json @home-assistant/supervisor
/tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
/homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core
@@ -1805,8 +1803,6 @@ build.json @home-assistant/supervisor
/tests/components/weatherflow_cloud/ @jeeftor
/homeassistant/components/weatherkit/ @tjhorner
/tests/components/weatherkit/ @tjhorner
/homeassistant/components/web_rtc/ @home-assistant/core
/tests/components/web_rtc/ @home-assistant/core
/homeassistant/components/webdav/ @jpbede
/tests/components/webdav/ @jpbede
/homeassistant/components/webhook/ @home-assistant/core

View File

@@ -30,7 +30,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
)
@@ -69,19 +68,34 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass,
cookie_jar=CookieJar(quote_cookie=False),
),
account_number=user_input[CONF_ACCOUNT_NUMBER],
account_number=user_input.get(CONF_ACCOUNT_NUMBER),
)
)
if isinstance(validation_response, BaseAuth):
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
account_number = (
user_input.get(CONF_ACCOUNT_NUMBER)
or validation_response.account_number
)
await self.async_set_unique_id(account_number)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_ACCOUNT_NUMBER],
title=account_number,
data={
**user_input,
CONF_ACCESS_TOKEN: validation_response.refresh_token,
CONF_ACCOUNT_NUMBER: account_number,
},
)
if validation_response == "smart_meter_unavailable":
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA.extend(
{
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
),
errors={"base": validation_response},
)
errors["base"] = validation_response
return self.async_show_form(

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==2.1.0"]
}

View File

@@ -21,29 +21,29 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.util.ssl import get_default_context
from .const import DOMAIN
from .websocket import BeoWebsocket
from .websocket import BangOlufsenWebsocket
@dataclass
class BeoData:
class BangOlufsenData:
"""Dataclass for API client and WebSocket client."""
websocket: BeoWebsocket
websocket: BangOlufsenWebsocket
client: MozartClient
type BeoConfigEntry = ConfigEntry[BeoData]
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
"""Set up from a config entry."""
# Remove casts to str
assert entry.unique_id
# Create device now as BeoWebsocket needs a device for debug logging, firing events etc.
# Create device now as BangOlufsenWebsocket needs a device for debug logging, firing events etc.
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
@@ -68,10 +68,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
await client.close_api_client()
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
websocket = BeoWebsocket(hass, entry, client)
websocket = BangOlufsenWebsocket(hass, entry, client)
# Add the websocket and API client
entry.runtime_data = BeoData(websocket, client)
entry.runtime_data = BangOlufsenData(websocket, client)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -82,7 +82,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: BangOlufsenConfigEntry
) -> bool:
"""Unload a config entry."""
# Close the API client and WebSocket notification listener
entry.runtime_data.client.disconnect_notifications()

View File

@@ -47,7 +47,7 @@ _exception_map = {
}
class BeoConfigFlowHandler(ConfigFlow, domain=DOMAIN):
class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
_beolink_jid = ""

View File

@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
)
class BeoSource:
class BangOlufsenSource:
"""Class used for associating device source ids with friendly names. May not include all sources."""
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
@@ -26,7 +26,7 @@ class BeoSource:
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
BEO_STATES: dict[str, MediaPlayerState] = {
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
# Dict used for translating device states to Home Assistant states.
"started": MediaPlayerState.PLAYING,
"buffering": MediaPlayerState.PLAYING,
@@ -40,19 +40,19 @@ BEO_STATES: dict[str, MediaPlayerState] = {
}
# Dict used for translating Home Assistant settings to device repeat settings.
BEO_REPEAT_FROM_HA: dict[RepeatMode, str] = {
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
RepeatMode.ALL: "all",
RepeatMode.ONE: "track",
RepeatMode.OFF: "none",
}
# Dict used for translating device repeat settings to Home Assistant settings.
BEO_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BEO_REPEAT_FROM_HA.items()
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
}
# Media types for play_media
class BeoMediaType(StrEnum):
class BangOlufsenMediaType(StrEnum):
"""Bang & Olufsen specific media types."""
FAVOURITE = "favourite"
@@ -63,7 +63,7 @@ class BeoMediaType(StrEnum):
OVERLAY_TTS = "overlay_tts"
class BeoModel(StrEnum):
class BangOlufsenModel(StrEnum):
"""Enum for compatible model names."""
# Mozart devices
@@ -82,7 +82,7 @@ class BeoModel(StrEnum):
BEOREMOTE_ONE = "Beoremote One"
class BeoAttribute(StrEnum):
class BangOlufsenAttribute(StrEnum):
"""Enum for extra_state_attribute keys."""
BEOLINK = "beolink"
@@ -93,7 +93,7 @@ class BeoAttribute(StrEnum):
# Physical "buttons" on devices
class BeoButtons(StrEnum):
class BangOlufsenButtons(StrEnum):
"""Enum for device buttons."""
BLUETOOTH = "Bluetooth"
@@ -140,7 +140,7 @@ class WebsocketNotification(StrEnum):
DOMAIN: Final[str] = "bang_olufsen"
# Default values for configuration.
DEFAULT_MODEL: Final[str] = BeoModel.BEOSOUND_BALANCE
DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE
# Configuration.
CONF_SERIAL_NUMBER: Final = "serial_number"
@@ -148,7 +148,7 @@ CONF_BEOLINK_JID: Final = "jid"
# Models to choose from in manual configuration.
SELECTABLE_MODELS: list[str] = [
model.value for model in BeoModel if model != BeoModel.BEOREMOTE_ONE
model.value for model in BangOlufsenModel if model != BangOlufsenModel.BEOREMOTE_ONE
]
MANUFACTURER: Final[str] = "Bang & Olufsen"
@@ -160,15 +160,15 @@ ATTR_ITEM_NUMBER: Final[str] = "in"
ATTR_FRIENDLY_NAME: Final[str] = "fn"
# Power states.
BEO_ON: Final[str] = "on"
BANG_OLUFSEN_ON: Final[str] = "on"
VALID_MEDIA_TYPES: Final[tuple] = (
BeoMediaType.FAVOURITE,
BeoMediaType.DEEZER,
BeoMediaType.RADIO,
BeoMediaType.TTS,
BeoMediaType.TIDAL,
BeoMediaType.OVERLAY_TTS,
BangOlufsenMediaType.FAVOURITE,
BangOlufsenMediaType.DEEZER,
BangOlufsenMediaType.RADIO,
BangOlufsenMediaType.TTS,
BangOlufsenMediaType.TIDAL,
BangOlufsenMediaType.OVERLAY_TTS,
MediaType.MUSIC,
MediaType.URL,
MediaType.CHANNEL,
@@ -246,7 +246,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
)
# Device events
BEO_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
EVENT_TRANSLATION_MAP: dict[str, str] = {
@@ -263,7 +263,7 @@ EVENT_TRANSLATION_MAP: dict[str, str] = {
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BeoButtons]
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BangOlufsenButtons]
DEVICE_BUTTON_EVENTS: Final[list[str]] = [

View File

@@ -10,13 +10,13 @@ from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import BeoConfigEntry
from . import BangOlufsenConfigEntry
from .const import DOMAIN
from .util import get_device_buttons
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: BeoConfigEntry
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""

View File

@@ -24,8 +24,8 @@ from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class BeoBase:
"""Base class for Bang & Olufsen Home Assistant objects."""
class BangOlufsenBase:
"""Base class for BangOlufsen Home Assistant objects."""
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
"""Initialize the object."""
@@ -51,8 +51,8 @@ class BeoBase:
)
class BeoEntity(Entity, BeoBase):
"""Base Entity for Bang & Olufsen entities."""
class BangOlufsenEntity(Entity, BangOlufsenBase):
"""Base Entity for BangOlufsen entities."""
_attr_has_entity_name = True
_attr_should_poll = False

View File

@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BeoConfigEntry
from . import BangOlufsenConfigEntry
from .const import (
BEO_REMOTE_CONTROL_KEYS,
BEO_REMOTE_KEY_EVENTS,
@@ -25,10 +25,10 @@ from .const import (
DEVICE_BUTTON_EVENTS,
DOMAIN,
MANUFACTURER,
BeoModel,
BangOlufsenModel,
WebsocketNotification,
)
from .entity import BeoEntity
from .entity import BangOlufsenEntity
from .util import get_device_buttons, get_remotes
PARALLEL_UPDATES = 0
@@ -36,14 +36,14 @@ PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BeoConfigEntry,
config_entry: BangOlufsenConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Event entities from config entry."""
entities: list[BeoEvent] = []
entities: list[BangOlufsenEvent] = []
async_add_entities(
BeoButtonEvent(config_entry, button_type)
BangOlufsenButtonEvent(config_entry, button_type)
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
)
@@ -54,7 +54,7 @@ async def async_setup_entry(
# Add Light keys
entities.extend(
[
BeoRemoteKeyEvent(
BangOlufsenRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
@@ -66,7 +66,7 @@ async def async_setup_entry(
# Add Control keys
entities.extend(
[
BeoRemoteKeyEvent(
BangOlufsenRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
@@ -84,9 +84,10 @@ async def async_setup_entry(
config_entry.entry_id
)
for device in devices:
if device.model == BeoModel.BEOREMOTE_ONE and device.serial_number not in {
remote.serial_number for remote in remotes
}:
if (
device.model == BangOlufsenModel.BEOREMOTE_ONE
and device.serial_number not in {remote.serial_number for remote in remotes}
):
device_registry.async_update_device(
device.id, remove_config_entry_id=config_entry.entry_id
)
@@ -94,13 +95,13 @@ async def async_setup_entry(
async_add_entities(new_entities=entities)
class BeoEvent(BeoEntity, EventEntity):
class BangOlufsenEvent(BangOlufsenEntity, EventEntity):
"""Base Event class."""
_attr_device_class = EventDeviceClass.BUTTON
_attr_entity_registry_enabled_default = False
def __init__(self, config_entry: BeoConfigEntry) -> None:
def __init__(self, config_entry: BangOlufsenConfigEntry) -> None:
"""Initialize Event."""
super().__init__(config_entry, config_entry.runtime_data.client)
@@ -111,12 +112,12 @@ class BeoEvent(BeoEntity, EventEntity):
self.async_write_ha_state()
class BeoButtonEvent(BeoEvent):
class BangOlufsenButtonEvent(BangOlufsenEvent):
"""Event class for Button events."""
_attr_event_types = DEVICE_BUTTON_EVENTS
def __init__(self, config_entry: BeoConfigEntry, button_type: str) -> None:
def __init__(self, config_entry: BangOlufsenConfigEntry, button_type: str) -> None:
"""Initialize Button."""
super().__init__(config_entry)
@@ -145,14 +146,14 @@ class BeoButtonEvent(BeoEvent):
)
class BeoRemoteKeyEvent(BeoEvent):
class BangOlufsenRemoteKeyEvent(BangOlufsenEvent):
"""Event class for Beoremote One key events."""
_attr_event_types = BEO_REMOTE_KEY_EVENTS
def __init__(
self,
config_entry: BeoConfigEntry,
config_entry: BangOlufsenConfigEntry,
remote: PairedRemote,
key_type: str,
) -> None:
@@ -165,8 +166,8 @@ class BeoRemoteKeyEvent(BeoEvent):
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
name=f"{BeoModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
model=BeoModel.BEOREMOTE_ONE,
name=f"{BangOlufsenModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
model=BangOlufsenModel.BEOREMOTE_ONE,
serial_number=remote.serial_number,
sw_version=remote.app_version,
manufacturer=MANUFACTURER,

View File

@@ -69,11 +69,11 @@ from homeassistant.helpers.entity_platform import (
)
from homeassistant.util.dt import utcnow
from . import BeoConfigEntry
from . import BangOlufsenConfigEntry
from .const import (
BEO_REPEAT_FROM_HA,
BEO_REPEAT_TO_HA,
BEO_STATES,
BANG_OLUFSEN_REPEAT_FROM_HA,
BANG_OLUFSEN_REPEAT_TO_HA,
BANG_OLUFSEN_STATES,
BEOLINK_JOIN_SOURCES,
BEOLINK_JOIN_SOURCES_TO_UPPER,
CONF_BEOLINK_JID,
@@ -82,12 +82,12 @@ from .const import (
FALLBACK_SOURCES,
MANUFACTURER,
VALID_MEDIA_TYPES,
BeoAttribute,
BeoMediaType,
BeoSource,
BangOlufsenAttribute,
BangOlufsenMediaType,
BangOlufsenSource,
WebsocketNotification,
)
from .entity import BeoEntity
from .entity import BangOlufsenEntity
from .util import get_serial_number_from_jid
PARALLEL_UPDATES = 0
@@ -96,7 +96,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
BEO_FEATURES = (
BANG_OLUFSEN_FEATURES = (
MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.GROUPING
@@ -119,13 +119,15 @@ BEO_FEATURES = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BeoConfigEntry,
config_entry: BangOlufsenConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Media Player entity from config entry."""
# Add MediaPlayer entity
async_add_entities(
new_entities=[BeoMediaPlayer(config_entry, config_entry.runtime_data.client)],
new_entities=[
BangOlufsenMediaPlayer(config_entry, config_entry.runtime_data.client)
],
update_before_add=True,
)
@@ -185,7 +187,7 @@ async def async_setup_entry(
)
class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Representation of a media player."""
_attr_name = None
@@ -286,7 +288,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
queue_settings = await self._client.get_settings_queue(_request_timeout=5)
if queue_settings.repeat is not None:
self._attr_repeat = BEO_REPEAT_TO_HA[queue_settings.repeat]
self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
if queue_settings.shuffle is not None:
self._attr_shuffle = queue_settings.shuffle
@@ -406,8 +408,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# Check if source is line-in or optical and progress should be updated
if self._source_change.id in (
BeoSource.LINE_IN.id,
BeoSource.SPDIF.id,
BangOlufsenSource.LINE_IN.id,
BangOlufsenSource.SPDIF.id,
):
self._playback_progress = PlaybackProgress(progress=0)
@@ -448,8 +450,10 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# Add Beolink self
self._beolink_attributes = {
BeoAttribute.BEOLINK: {
BeoAttribute.BEOLINK_SELF: {self.device_entry.name: self._beolink_jid}
BangOlufsenAttribute.BEOLINK: {
BangOlufsenAttribute.BEOLINK_SELF: {
self.device_entry.name: self._beolink_jid
}
}
}
@@ -457,12 +461,12 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
peers = await self._client.get_beolink_peers()
if len(peers) > 0:
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BangOlufsenAttribute.BEOLINK_PEERS
] = {}
for peer in peers:
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BangOlufsenAttribute.BEOLINK_PEERS
][peer.friendly_name] = peer.jid
# Add Beolink listeners / leader
@@ -484,8 +488,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# Add self
group_members.append(self.entity_id)
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_LEADER
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BangOlufsenAttribute.BEOLINK_LEADER
] = {
self._remote_leader.friendly_name: self._remote_leader.jid,
}
@@ -523,8 +527,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
beolink_listener.jid
)
break
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_LISTENERS
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BangOlufsenAttribute.BEOLINK_LISTENERS
] = beolink_listeners_attribute
self._attr_group_members = group_members
@@ -596,7 +600,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
features = BEO_FEATURES
features = BANG_OLUFSEN_FEATURES
# Add seeking if supported by the current source
if self._source_change.is_seekable is True:
@@ -607,7 +611,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
@property
def state(self) -> MediaPlayerState:
"""Return the current state of the media player."""
return BEO_STATES[self._state]
return BANG_OLUFSEN_STATES[self._state]
@property
def volume_level(self) -> float | None:
@@ -627,10 +631,10 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
def media_content_type(self) -> MediaType | str | None:
"""Return the current media type."""
content_type = {
BeoSource.URI_STREAMER.id: MediaType.URL,
BeoSource.DEEZER.id: BeoMediaType.DEEZER,
BeoSource.TIDAL.id: BeoMediaType.TIDAL,
BeoSource.NET_RADIO.id: BeoMediaType.RADIO,
BangOlufsenSource.URI_STREAMER.id: MediaType.URL,
BangOlufsenSource.DEEZER.id: BangOlufsenMediaType.DEEZER,
BangOlufsenSource.TIDAL.id: BangOlufsenMediaType.TIDAL,
BangOlufsenSource.NET_RADIO.id: BangOlufsenMediaType.RADIO,
}
# Hard to determine content type.
if self._source_change.id in content_type:
@@ -761,7 +765,9 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set playback queues to repeat."""
await self._client.set_settings_queue(
play_queue_settings=PlayQueueSettings(repeat=BEO_REPEAT_FROM_HA[repeat])
play_queue_settings=PlayQueueSettings(
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
)
)
async def async_set_shuffle(self, shuffle: bool) -> None:
@@ -865,7 +871,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
self._volume.level.level + offset_volume, 100
)
if media_type == BeoMediaType.OVERLAY_TTS:
if media_type == BangOlufsenMediaType.OVERLAY_TTS:
# Bang & Olufsen cloud TTS
overlay_play_request.text_to_speech = (
OverlayPlayRequestTextToSpeechTextToSpeech(
@@ -882,14 +888,14 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# The "provider" media_type may not be suitable for overlay all the time.
# Use it for now.
elif media_type == BeoMediaType.TTS:
elif media_type == BangOlufsenMediaType.TTS:
await self._client.post_overlay_play(
overlay_play_request=OverlayPlayRequest(
uri=Uri(location=media_id),
)
)
elif media_type == BeoMediaType.RADIO:
elif media_type == BangOlufsenMediaType.RADIO:
await self._client.run_provided_scene(
scene_properties=SceneProperties(
action_list=[
@@ -901,13 +907,13 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
)
)
elif media_type == BeoMediaType.FAVOURITE:
elif media_type == BangOlufsenMediaType.FAVOURITE:
await self._client.activate_preset(id=int(media_id))
elif media_type in (BeoMediaType.DEEZER, BeoMediaType.TIDAL):
elif media_type in (BangOlufsenMediaType.DEEZER, BangOlufsenMediaType.TIDAL):
try:
# Play Deezer flow.
if media_id == "flow" and media_type == BeoMediaType.DEEZER:
if media_id == "flow" and media_type == BangOlufsenMediaType.DEEZER:
deezer_id = None
if "id" in kwargs[ATTR_MEDIA_EXTRA]:

View File

@@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DEVICE_BUTTONS, DOMAIN, BeoButtons, BeoModel
from .const import DEVICE_BUTTONS, DOMAIN, BangOlufsenButtons, BangOlufsenModel
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
@@ -40,16 +40,16 @@ async def get_remotes(client: MozartClient) -> list[PairedRemote]:
]
def get_device_buttons(model: BeoModel) -> list[str]:
def get_device_buttons(model: BangOlufsenModel) -> list[str]:
"""Get supported buttons for a given model."""
buttons = DEVICE_BUTTONS.copy()
# Beosound Premiere does not have a bluetooth button
if model == BeoModel.BEOSOUND_PREMIERE:
buttons.remove(BeoButtons.BLUETOOTH)
if model == BangOlufsenModel.BEOSOUND_PREMIERE:
buttons.remove(BangOlufsenButtons.BLUETOOTH)
# Beoconnect Core does not have any buttons
elif model == BeoModel.BEOCONNECT_CORE:
elif model == BangOlufsenModel.BEOCONNECT_CORE:
buttons = []
return buttons

View File

@@ -27,20 +27,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.enum import try_parse_enum
from .const import (
BEO_WEBSOCKET_EVENT,
BANG_OLUFSEN_WEBSOCKET_EVENT,
CONNECTION_STATUS,
DOMAIN,
EVENT_TRANSLATION_MAP,
BeoModel,
BangOlufsenModel,
WebsocketNotification,
)
from .entity import BeoBase
from .entity import BangOlufsenBase
from .util import get_device, get_remotes
_LOGGER = logging.getLogger(__name__)
class BeoWebsocket(BeoBase):
class BangOlufsenWebsocket(BangOlufsenBase):
"""The WebSocket listeners."""
def __init__(
@@ -48,7 +48,7 @@ class BeoWebsocket(BeoBase):
) -> None:
"""Initialize the WebSocket listeners."""
BeoBase.__init__(self, entry, client)
BangOlufsenBase.__init__(self, entry, client)
self.hass = hass
self._device = get_device(hass, self._unique_id)
@@ -178,7 +178,7 @@ class BeoWebsocket(BeoBase):
self.entry.entry_id
)
if device.serial_number is not None
and device.model == BeoModel.BEOREMOTE_ONE
and device.model == BangOlufsenModel.BEOREMOTE_ONE
]
# Get paired remotes from device
remote_serial_numbers = [
@@ -274,4 +274,4 @@ class BeoWebsocket(BeoBase):
}
_LOGGER.debug("%s", debug_notification)
self.hass.bus.async_fire(BEO_WEBSOCKET_EVENT, debug_notification)
self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, debug_notification)

View File

@@ -15,7 +15,7 @@
],
"quality_scale": "internal",
"requirements": [
"bleak==2.0.0",
"bleak==1.0.1",
"bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",

View File

@@ -20,7 +20,7 @@ from aiohttp import hdrs, web
import attr
from propcache.api import cached_property, under_cached_property
import voluptuous as vol
from webrtc_models import RTCIceCandidateInit
from webrtc_models import RTCIceCandidateInit, RTCIceServer
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
@@ -37,7 +37,6 @@ from homeassistant.components.stream import (
Stream,
create_stream,
)
from homeassistant.components.web_rtc import async_get_ice_servers
from homeassistant.components.websocket_api import ActiveConnection
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -85,6 +84,7 @@ from .prefs import (
get_dynamic_camera_stream_settings,
)
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCProvider,
WebRTCAnswer, # noqa: F401
WebRTCCandidate, # noqa: F401
@@ -93,6 +93,7 @@ from .webrtc import (
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_provider,
async_register_ice_servers,
async_register_webrtc_provider, # noqa: F401
async_register_ws,
)
@@ -399,6 +400,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
SERVICE_RECORD, CAMERA_SERVICE_RECORD, async_handle_record_service
)
@callback
def get_ice_servers() -> list[RTCIceServer]:
if hass.config.webrtc.ice_servers:
return hass.config.webrtc.ice_servers
return [
RTCIceServer(
urls=[
"stun:stun.home-assistant.io:3478",
"stun:stun.home-assistant.io:80",
]
),
]
async_register_ice_servers(hass, get_ice_servers)
return True
@@ -716,7 +731,11 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
config = self._async_get_webrtc_client_configuration()
ice_servers = async_get_ice_servers(self.hass)
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
return config

View File

@@ -3,7 +3,7 @@
"name": "Camera",
"after_dependencies": ["media_player"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["http", "web_rtc"],
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps
import logging
@@ -12,7 +12,12 @@ from typing import TYPE_CHECKING, Any
from mashumaro import MissingField
import voluptuous as vol
from webrtc_models import RTCConfiguration, RTCIceCandidate, RTCIceCandidateInit
from webrtc_models import (
RTCConfiguration,
RTCIceCandidate,
RTCIceCandidateInit,
RTCIceServer,
)
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
@@ -33,6 +38,9 @@ _LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"camera_webrtc_ice_servers"
)
_WEBRTC = "WebRTC"
@@ -359,3 +367,21 @@ async def async_get_supported_provider(
return provider
return None
@callback
def async_register_ice_servers(
hass: HomeAssistant,
get_ice_server_fn: Callable[[], Iterable[RTCIceServer]],
) -> Callable[[], None]:
"""Register a ICE server.
The registering integration is responsible to implement caching if needed.
"""
servers = hass.data.setdefault(DATA_ICE_SERVERS, [])
def remove() -> None:
servers.remove(get_ice_server_fn)
servers.append(get_ice_server_fn)
return remove

View File

@@ -19,8 +19,8 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
from homeassistant.components.camera import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.components.web_rtc import async_register_ice_servers
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE

View File

@@ -8,7 +8,7 @@
"google_assistant"
],
"codeowners": ["@home-assistant/cloud"],
"dependencies": ["auth", "http", "repairs", "webhook", "web_rtc"],
"dependencies": ["auth", "http", "repairs", "webhook"],
"documentation": "https://www.home-assistant.io/integrations/cloud",
"integration_type": "system",
"iot_class": "cloud_push",

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==16.4.0"]
}

View File

@@ -15,9 +15,7 @@ from .coordinator import (
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.DEVICE_TRACKER,
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@@ -34,6 +34,13 @@ BINARY_SENSOR_ENTITY_DESCRIPTIONS: tuple[
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.charging,
),
FressnapfTrackerBinarySensorDescription(
translation_key="deep_sleep",
key="deep_sleep_value",
device_class=BinarySensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: bool(data.deep_sleep_value),
),
)

View File

@@ -4,14 +4,6 @@
"pet": {
"default": "mdi:paw"
}
},
"switch": {
"energy_saving": {
"default": "mdi:sleep",
"state": {
"off": "mdi:sleep-off"
}
}
}
}
}

View File

@@ -1,93 +0,0 @@
"""Light platform for fressnapf_tracker."""
from typing import TYPE_CHECKING, Any
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ColorMode,
LightEntity,
LightEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .const import DOMAIN
from .entity import FressnapfTrackerEntity
LIGHT_ENTITY_DESCRIPTION = LightEntityDescription(
translation_key="led",
entity_category=EntityCategory.CONFIG,
key="led_brightness_value",
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FressnapfTrackerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fressnapf Tracker lights."""
async_add_entities(
FressnapfTrackerLight(coordinator, LIGHT_ENTITY_DESCRIPTION)
for coordinator in entry.runtime_data
if coordinator.data.led_activatable is not None
and coordinator.data.led_activatable.has_led
and coordinator.data.tracker_settings.features.flash_light
)
class FressnapfTrackerLight(FressnapfTrackerEntity, LightEntity):
"""Fressnapf Tracker light."""
_attr_color_mode: ColorMode = ColorMode.BRIGHTNESS
_attr_supported_color_modes: set[ColorMode] = {ColorMode.BRIGHTNESS}
@property
def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
if TYPE_CHECKING:
# The entity is not created if led_brightness_value is None
assert self.coordinator.data.led_brightness_value is not None
return int(round((self.coordinator.data.led_brightness_value / 100) * 255))
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the device."""
self.raise_if_not_activatable()
brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
brightness = int((brightness / 255) * 100)
await self.coordinator.client.set_led_brightness(brightness)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the device."""
await self.coordinator.client.set_led_brightness(0)
await self.coordinator.async_request_refresh()
def raise_if_not_activatable(self) -> None:
"""Raise error with reasoning if light is not activatable."""
if TYPE_CHECKING:
# The entity is not created if led_activatable is None
assert self.coordinator.data.led_activatable is not None
error_type: str | None = None
if not self.coordinator.data.led_activatable.seen_recently:
error_type = "not_seen_recently"
elif not self.coordinator.data.led_activatable.not_charging:
error_type = "charging"
elif not self.coordinator.data.led_activatable.nonempty_battery:
error_type = "low_battery"
if error_type is not None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=error_type,
)
@property
def is_on(self) -> bool:
"""Return true if device is on."""
if self.coordinator.data.led_brightness_value is not None:
return self.coordinator.data.led_brightness_value > 0
return False

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["fressnapftracker==0.2.0"]
"requirements": ["fressnapftracker==0.1.2"]
}

View File

@@ -47,26 +47,10 @@
}
},
"entity": {
"light": {
"led": {
"name": "Flashlight"
"binary_sensor": {
"deep_sleep": {
"name": "Deep sleep"
}
},
"switch": {
"energy_saving": {
"name": "Sleep mode"
}
}
},
"exceptions": {
"charging": {
"message": "The flashlight cannot be activated while charging."
},
"low_battery": {
"message": "The flashlight cannot be activated due to low battery."
},
"not_seen_recently": {
"message": "The flashlight cannot be activated when the tracker has not moved recently."
}
}
}

View File

@@ -1,58 +0,0 @@
"""Switch platform for Fressnapf Tracker."""
from typing import TYPE_CHECKING, Any
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .entity import FressnapfTrackerEntity
SWITCH_ENTITY_DESCRIPTION = SwitchEntityDescription(
translation_key="energy_saving",
entity_category=EntityCategory.CONFIG,
device_class=SwitchDeviceClass.SWITCH,
key="energy_saving",
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FressnapfTrackerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fressnapf Tracker switches."""
async_add_entities(
FressnapfTrackerSwitch(coordinator, SWITCH_ENTITY_DESCRIPTION)
for coordinator in entry.runtime_data
if coordinator.data.tracker_settings.features.energy_saving_mode
)
class FressnapfTrackerSwitch(FressnapfTrackerEntity, SwitchEntity):
"""Fressnapf Tracker switch."""
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the device."""
await self.coordinator.client.set_energy_saving(True)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the device."""
await self.coordinator.client.set_energy_saving(False)
await self.coordinator.async_request_refresh()
@property
def is_on(self) -> bool:
"""Return true if device is on."""
if TYPE_CHECKING:
# The entity is not created if energy_saving is None
assert self.coordinator.data.energy_saving is not None
return self.coordinator.data.energy_saving.value == 1

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251203.0"]
"requirements": ["home-assistant-frontend==20251202.0"]
}

View File

@@ -1,58 +0,0 @@
"""The homelink integration."""
from __future__ import annotations
from homelink.mqtt_provider import MQTTProvider
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from . import oauth2
from .const import DOMAIN
from .coordinator import HomeLinkConfigEntry, HomeLinkCoordinator, HomeLinkData
PLATFORMS: list[Platform] = [Platform.EVENT]
async def async_setup_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) -> bool:
"""Set up homelink from a config entry."""
auth_implementation = oauth2.SRPAuthImplementation(hass, DOMAIN)
config_entry_oauth2_flow.async_register_implementation(
hass, DOMAIN, auth_implementation
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
authenticated_session = oauth2.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass), session
)
provider = MQTTProvider(authenticated_session)
coordinator = HomeLinkCoordinator(hass, provider, entry)
entry.async_on_unload(
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, coordinator.async_on_unload
)
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = HomeLinkData(
provider=provider, coordinator=coordinator, last_update_id=None
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) -> bool:
"""Unload a config entry."""
await entry.runtime_data.coordinator.async_on_unload(None)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,14 +0,0 @@
"""application_credentials platform for the gentex homelink integration."""
from homeassistant.components.application_credentials import ClientCredential
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from . import oauth2
async def async_get_auth_implementation(
hass: HomeAssistant, auth_domain: str, _credential: ClientCredential
) -> config_entry_oauth2_flow.AbstractOAuth2Implementation:
"""Return custom SRPAuth implementation."""
return oauth2.SRPAuthImplementation(hass, auth_domain)

View File

@@ -1,66 +0,0 @@
"""Config flow for homelink."""
import logging
from typing import Any
import botocore.exceptions
from homelink.auth.srp_auth import SRPAuth
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
from .const import DOMAIN
from .oauth2 import SRPAuthImplementation
_LOGGER = logging.getLogger(__name__)
class SRPFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Config flow to handle homelink OAuth2 authentication."""
DOMAIN = DOMAIN
def __init__(self) -> None:
"""Set up the flow handler."""
super().__init__()
self.flow_impl = SRPAuthImplementation(self.hass, DOMAIN)
@property
def logger(self):
"""Get the logger."""
return _LOGGER
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Ask for username and password."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
srp_auth = SRPAuth()
try:
tokens = await self.hass.async_add_executor_job(
srp_auth.async_get_access_token,
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
)
except botocore.exceptions.ClientError:
_LOGGER.exception("Error authenticating homelink account")
errors["base"] = "srp_auth_failed"
except Exception:
_LOGGER.exception("An unexpected error occurred")
errors["base"] = "unknown"
else:
self.external_data = {"tokens": tokens}
return await self.async_step_creation()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
),
errors=errors,
)

View File

@@ -1,7 +0,0 @@
"""Constants for the homelink integration."""
DOMAIN = "gentex_homelink"
OAUTH2_TOKEN = "https://auth.homelinkcloud.com/oauth2/token"
POLLING_INTERVAL = 5
EVENT_PRESSED = "Pressed"

View File

@@ -1,113 +0,0 @@
"""Makes requests to the state server and stores the resulting data so that the buttons can access it."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from functools import partial
import logging
from typing import TYPE_CHECKING, TypedDict
from homelink.model.device import Device
from homelink.mqtt_provider import MQTTProvider
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.util.ssl import get_default_context
if TYPE_CHECKING:
from .event import HomeLinkEventEntity
_LOGGER = logging.getLogger(__name__)
type HomeLinkConfigEntry = ConfigEntry[HomeLinkData]
type EventCallback = Callable[[HomeLinkEventData], None]
@dataclass
class HomeLinkData:
"""Class for HomeLink integration runtime data."""
provider: MQTTProvider
coordinator: HomeLinkCoordinator
last_update_id: str | None
class HomeLinkEventData(TypedDict):
"""Data for a single event."""
requestId: str
timestamp: int
class HomeLinkMQTTMessage(TypedDict):
"""HomeLink MQTT Event message."""
type: str
data: dict[str, HomeLinkEventData] # Each key is a button id
class HomeLinkCoordinator:
"""HomeLink integration coordinator."""
def __init__(
self,
hass: HomeAssistant,
provider: MQTTProvider,
config_entry: HomeLinkConfigEntry,
) -> None:
"""Initialize my coordinator."""
self.hass = hass
self.config_entry = config_entry
self.provider = provider
self.device_data: list[Device] = []
self.buttons: list[HomeLinkEventEntity] = []
self._listeners: dict[str, EventCallback] = {}
@callback
def async_add_event_listener(
self, update_callback: EventCallback, target_event_id: str
) -> Callable[[], None]:
"""Listen for updates."""
self._listeners[target_event_id] = update_callback
return partial(self.__async_remove_listener_internal, target_event_id)
def __async_remove_listener_internal(self, listener_id: str):
del self._listeners[listener_id]
@callback
def async_handle_state_data(self, data: dict[str, HomeLinkEventData]):
"""Notify listeners."""
for button_id, event in data.items():
if listener := self._listeners.get(button_id):
listener(event)
async def async_config_entry_first_refresh(self) -> None:
"""Refresh data for the first time when a config entry is setup."""
await self._async_setup()
async def async_on_unload(self, _event):
"""Disconnect and unregister when unloaded."""
await self.provider.disable()
async def _async_setup(self) -> None:
"""Set up the coordinator."""
await self.provider.enable(get_default_context())
await self.discover_devices()
self.provider.listen(self.on_message)
async def discover_devices(self):
"""Discover devices and build the Entities."""
self.device_data = await self.provider.discover()
def on_message(
self: HomeLinkCoordinator, _topic: str, message: HomeLinkMQTTMessage
):
"MQTT Callback function."
if message["type"] == "state":
self.hass.add_job(self.async_handle_state_data, message["data"])
if message["type"] == "requestSync":
self.hass.add_job(
self.hass.config_entries.async_reload,
self.config_entry.entry_id,
)

View File

@@ -1,83 +0,0 @@
"""Platform for Event integration."""
from __future__ import annotations
from homeassistant.components.event import EventDeviceClass, EventEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, EVENT_PRESSED
from .coordinator import HomeLinkCoordinator, HomeLinkEventData
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the entities for the binary sensor."""
coordinator = config_entry.runtime_data.coordinator
for device in coordinator.device_data:
buttons = [
HomeLinkEventEntity(b.id, b.name, device.id, device.name, coordinator)
for b in device.buttons
]
coordinator.buttons.extend(buttons)
async_add_entities(coordinator.buttons)
# Updates are centralized by the coordinator.
PARALLEL_UPDATES = 0
class HomeLinkEventEntity(EventEntity):
"""Event Entity."""
_attr_has_entity_name = True
_attr_event_types = [EVENT_PRESSED]
_attr_device_class = EventDeviceClass.BUTTON
def __init__(
self,
id: str,
param_name: str,
device_id: str,
device_name: str,
coordinator: HomeLinkCoordinator,
) -> None:
"""Initialize the event entity."""
self.id: str = id
self._attr_name: str = param_name
self._attr_unique_id: str = id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=device_name,
)
self.coordinator = coordinator
self.last_request_id: str | None = None
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.async_add_event_listener(
self._handle_event_data_update, self.id
)
)
@callback
def _handle_event_data_update(self, update_data: HomeLinkEventData) -> None:
"""Update this button."""
if update_data["requestId"] != self.last_request_id:
self._trigger_event(EVENT_PRESSED)
self.last_request_id = update_data["requestId"]
self.async_write_ha_state()
async def async_update(self):
"""Request early polling. Left intentionally blank because it's not possible in this implementation."""

View File

@@ -1,11 +0,0 @@
{
"domain": "gentex_homelink",
"name": "HomeLink",
"codeowners": ["@niaexa", "@ryanjones-gentex"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/gentex_homelink",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["homelink-integration-api==0.0.1"]
}

View File

@@ -1,114 +0,0 @@
"""API for homelink bound to Home Assistant OAuth."""
from json import JSONDecodeError
import logging
import time
from typing import cast
from aiohttp import ClientError, ClientSession
from homelink.auth.abstract_auth import AbstractAuth
from homelink.settings import COGNITO_CLIENT_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import OAUTH2_TOKEN
_LOGGER = logging.getLogger(__name__)
class SRPAuthImplementation(config_entry_oauth2_flow.AbstractOAuth2Implementation):
"""Base class to abstract OAuth2 authentication."""
def __init__(self, hass: HomeAssistant, domain) -> None:
"""Initialize the SRP Auth implementation."""
self.hass = hass
self._domain = domain
self.client_id = COGNITO_CLIENT_ID
@property
def name(self) -> str:
"""Name of the implementation."""
return "SRPAuth"
@property
def domain(self) -> str:
"""Domain that is providing the implementation."""
return self._domain
async def async_generate_authorize_url(self, flow_id: str) -> str:
"""Left intentionally blank because the auth is handled by SRP."""
return ""
async def async_resolve_external_data(self, external_data) -> dict:
"""Format the token from the source appropriately for HomeAssistant."""
tokens = external_data["tokens"]
new_token = {}
new_token["access_token"] = tokens["AuthenticationResult"]["AccessToken"]
new_token["refresh_token"] = tokens["AuthenticationResult"]["RefreshToken"]
new_token["token_type"] = tokens["AuthenticationResult"]["TokenType"]
new_token["expires_in"] = tokens["AuthenticationResult"]["ExpiresIn"]
new_token["expires_at"] = (
time.time() + tokens["AuthenticationResult"]["ExpiresIn"]
)
return new_token
async def _token_request(self, data: dict) -> dict:
"""Make a token request."""
session = async_get_clientsession(self.hass)
data["client_id"] = self.client_id
_LOGGER.debug("Sending token request to %s", OAUTH2_TOKEN)
resp = await session.post(OAUTH2_TOKEN, data=data)
if resp.status >= 400:
try:
error_response = await resp.json()
except (ClientError, JSONDecodeError):
error_response = {}
error_code = error_response.get("error", "unknown")
error_description = error_response.get(
"error_description", "unknown error"
)
_LOGGER.error(
"Token request for %s failed (%s): %s",
self.domain,
error_code,
error_description,
)
resp.raise_for_status()
return cast(dict, await resp.json())
async def _async_refresh_token(self, token: dict) -> dict:
"""Refresh tokens."""
new_token = await self._token_request(
{
"grant_type": "refresh_token",
"client_id": self.client_id,
"refresh_token": token["refresh_token"],
}
)
return {**token, **new_token}
class AsyncConfigEntryAuth(AbstractAuth):
"""Provide homelink authentication tied to an OAuth2 based config entry."""
def __init__(
self,
websession: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
) -> None:
"""Initialize homelink auth."""
super().__init__(websession)
self._oauth_session = oauth_session
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
if not self._oauth_session.valid_token:
await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]

View File

@@ -1,76 +0,0 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register any service actions
appropriate-polling:
status: exempt
comment: Integration does not poll
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register any service actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: Integration does not register any service actions
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: It is not necessary to update IP addresses of devices or services in this Integration
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: done
entity-category: todo
entity-device-class: todo
entity-disabled-by-default:
status: exempt
comment: Entities are not noisy and are expected to be enabled by default
entity-translations:
status: exempt
comment: Entity properties are user-defined, and therefore cannot be translated
exception-translations: todo
icon-translations:
status: exempt
comment: Entities in this integration do not use icons, and therefore do not require translation
reconfiguration-flow: todo
repair-issues: todo
stale-devices: done
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -1,38 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
},
"error": {
"srp_auth_failed": "Error authenticating HomeLink account",
"unknown": "An unknown error occurred. Please try again later"
},
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "Email address associated with your HomeLink account",
"password": "Password associated with your HomeLink account"
}
}
}
}
}

View File

@@ -59,14 +59,9 @@
"user": "Add location"
},
"step": {
"location": {
"user": {
"data": {
"location": "[%key:common::config_flow::data::location%]",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"location": "[%key:component::google_air_quality::config::step::user::data_description::location%]",
"name": "[%key:component::google_air_quality::config::step::user::data_description::name%]"
"location": "[%key:common::config_flow::data::location%]"
},
"description": "Select the coordinates for which you want to create an entry.",
"title": "Air quality data location"

View File

@@ -72,7 +72,7 @@
"title": "The blinker fluid is empty and needs to be refilled"
},
"special_repair": {
"description": "This is a special repair created by a preview feature! This demonstrates how Labs features can interact with the Home Assistant repair system. You can disable this by turning off the kitchen sink special repair feature in Settings > System > Labs.",
"description": "This is a special repair created by a preview feature! This demonstrates how lab features can interact with the Home Assistant repair system. You can disable this by turning off the kitchen sink special repair feature in Settings > System > Labs.",
"title": "Special repair feature preview"
},
"transmogrifier_deprecated": {
@@ -109,7 +109,7 @@
},
"preview_features": {
"special_repair": {
"description": "Creates a **special repair issue** when enabled.\n\nThis demonstrates how Labs features can interact with other Home Assistant integrations.",
"description": "Creates a **special repair issue** when enabled.\n\nThis demonstrates how lab features can interact with other Home Assistant integrations.",
"disable_confirmation": "This will remove the special repair issue. Don't worry, this is just a demonstration feature.",
"enable_confirmation": "This will create a special repair issue to demonstrate Labs preview features. This is just an example and won't affect your actual system.",
"name": "Special repair"

View File

@@ -21,6 +21,9 @@
"telegram_count": {
"default": "mdi:plus-network"
},
"telegrams_data_secure_undecodable": {
"default": "mdi:lock-alert"
},
"telegrams_incoming": {
"default": "mdi:upload-network"
},

View File

@@ -108,6 +108,12 @@ SYSTEM_ENTITY_DESCRIPTIONS = (
+ knx.xknx.connection_manager.cemi_count_incoming
+ knx.xknx.connection_manager.cemi_count_incoming_error,
),
KNXSystemEntityDescription(
key="telegrams_data_secure_undecodable",
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda knx: knx.xknx.connection_manager.undecoded_data_secure,
),
)

View File

@@ -639,6 +639,10 @@
"name": "Telegrams",
"unit_of_measurement": "telegrams"
},
"telegrams_data_secure_undecodable": {
"name": "Undecodable Data Secure telegrams",
"unit_of_measurement": "[%key:component::knx::entity::sensor::telegrams_incoming_error::unit_of_measurement%]"
},
"telegrams_incoming": {
"name": "Incoming telegrams",
"unit_of_measurement": "[%key:component::knx::entity::sensor::telegram_count::unit_of_measurement%]"

View File

@@ -86,12 +86,6 @@
"current_phase": {
"default": "mdi:state-machine"
},
"door_closed_events": {
"default": "mdi:door-closed"
},
"door_open_events": {
"default": "mdi:door-open"
},
"esa_opt_out_state": {
"default": "mdi:home-lightning-bolt"
},

View File

@@ -1488,30 +1488,4 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterSensor,
required_attributes=(clusters.ServiceArea.Attributes.EstimatedEndTime,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="DoorLockDoorOpenEvents",
translation_key="door_open_events",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.DoorOpenEvents,),
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="DoorLockDoorClosedEvents",
translation_key="door_closed_events",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.DoorClosedEvents,),
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
),
]

View File

@@ -375,12 +375,6 @@
"current_phase": {
"name": "Current phase"
},
"door_closed_events": {
"name": "Door closed events"
},
"door_open_events": {
"name": "Door open events"
},
"energy_exported": {
"name": "Energy exported"
},

View File

@@ -81,9 +81,6 @@ async def async_setup_entry(
SERVICE_PUBLISH,
SERVICE_PUBLISH_SCHEMA,
"publish",
description_placeholders={
"markdown_guide_url": "https://www.markdownguide.org/basic-syntax/"
},
)

View File

@@ -345,7 +345,7 @@
"name": "Icon URL"
},
"markdown": {
"description": "Enable Markdown formatting for the message body. See the Markdown guide for syntax details: {markdown_guide_url}.",
"description": "Enable Markdown formatting for the message body. See the Markdown guide for syntax details: https://www.markdownguide.org/basic-syntax/.",
"name": "Format as Markdown"
},
"message": {

View File

@@ -25,7 +25,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.async_ import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__)
@@ -102,18 +101,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
except OSError:
_LOGGER.error("Pilight send failed for %s", str(message_data))
def _register_service() -> None:
hass.services.async_register(
DOMAIN,
SERVICE_NAME,
send_code,
schema=RF_CODE_SCHEMA,
description_placeholders={
"pilight_protocols_docs_url": "https://manual.pilight.org/protocols/index.html"
},
)
run_callback_threadsafe(hass.loop, _register_service).result()
hass.services.register(DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA)
# Publish received codes on the HA event bus
# A whitelist of codes to be published in the event bus

View File

@@ -4,7 +4,7 @@
"description": "Sends RF code to Pilight device.",
"fields": {
"protocol": {
"description": "Protocol that Pilight recognizes. See {pilight_protocols_docs_url} for supported protocols and additional parameters that each protocol supports.",
"description": "Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports.",
"name": "Protocol"
}
},

View File

@@ -251,7 +251,13 @@ class PlaystationNetworkFriendDataCoordinator(
def _update_data(self) -> PlaystationNetworkData:
"""Update friend status data."""
try:
presence = self.user.get_presence()
return PlaystationNetworkData(
username=self.user.online_id,
account_id=self.user.account_id,
presence=self.user.get_presence(),
profile=self.profile,
trophy_summary=self.user.trophy_summary(),
)
except PSNAWPForbiddenError as error:
raise UpdateFailed(
translation_domain=DOMAIN,
@@ -261,19 +267,6 @@ class PlaystationNetworkFriendDataCoordinator(
except PSNAWPError:
raise
try:
trophy_summary = self.user.trophy_summary()
except PSNAWPForbiddenError:
trophy_summary = None
return PlaystationNetworkData(
username=self.user.online_id,
account_id=self.user.account_id,
profile=self.profile,
presence=presence,
trophy_summary=trophy_summary,
)
async def update_data(self) -> PlaystationNetworkData:
"""Update friend status data."""
return await self.hass.async_add_executor_job(self._update_data)

View File

@@ -54,12 +54,9 @@ from .const import (
)
from .coordinator import RainMachineDataUpdateCoordinator
API_URL_REFERENCE = (
"https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post"
)
DEFAULT_SSL = True
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
@@ -458,15 +455,7 @@ async def async_setup_entry( # noqa: C901
):
if hass.services.has_service(DOMAIN, service_name):
continue
hass.services.async_register(
DOMAIN,
service_name,
method,
schema=schema,
description_placeholders={
"api_url": API_URL_REFERENCE,
},
)
hass.services.async_register(DOMAIN, service_name, method, schema=schema)
return True

View File

@@ -128,7 +128,7 @@
"name": "Push flow meter data"
},
"push_weather_data": {
"description": "Sends weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integration.\nSee details of RainMachine API here: {api_url}",
"description": "Sends weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integration.\nSee details of RainMachine API here: https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post.",
"fields": {
"condition": {
"description": "Current weather condition code (WNUM).",

View File

@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.17.1"]
"requirements": ["reolink-aio==0.16.6"]
}

View File

@@ -17,7 +17,6 @@ from roborock import (
from roborock.data import UserData
from roborock.devices.device import RoborockDevice
from roborock.devices.device_manager import UserParams, create_device_manager
from roborock.map.map_parser import MapParserConfig
from homeassistant.const import CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
@@ -25,16 +24,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
CONF_BASE_URL,
CONF_SHOW_BACKGROUND,
CONF_USER_DATA,
DEFAULT_DRAWABLES,
DOMAIN,
DRAWABLES,
MAP_SCALE,
PLATFORMS,
)
from .const import CONF_BASE_URL, CONF_USER_DATA, DOMAIN, PLATFORMS
from .coordinator import (
RoborockConfigEntry,
RoborockCoordinators,
@@ -66,15 +56,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
user_params,
cache=cache,
session=async_get_clientsession(hass),
map_parser_config=MapParserConfig(
drawables=[
drawable
for drawable, default_value in DEFAULT_DRAWABLES.items()
if entry.options.get(DRAWABLES, {}).get(drawable, default_value)
],
show_background=entry.options.get(CONF_SHOW_BACKGROUND, False),
map_scale=MAP_SCALE,
),
)
except RoborockInvalidCredentials as err:
raise ConfigEntryAuthFailed(

View File

@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==3.10.0",
"python-roborock==3.9.2",
"vacuum-map-parser-roborock==0.1.4"
]
}

View File

@@ -7,7 +7,6 @@ from dataclasses import dataclass
from pysmartthings import Capability, Command, SmartThings
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -34,13 +33,6 @@ CAPABILITIES_TO_BUTTONS: dict[Capability | str, SmartThingsButtonDescription] =
key=Capability.CUSTOM_WATER_FILTER,
translation_key="reset_water_filter",
command=Command.RESET_WATER_FILTER,
entity_category=EntityCategory.DIAGNOSTIC,
),
Capability.SAMSUNG_CE_HOOD_FILTER: SmartThingsButtonDescription(
key=Capability.SAMSUNG_CE_HOOD_FILTER,
translation_key="reset_hood_filter",
command=Command.RESET_HOOD_FILTER,
entity_category=EntityCategory.DIAGNOSTIC,
),
}

View File

@@ -74,9 +74,6 @@
}
},
"button": {
"reset_hood_filter": {
"name": "Reset filter"
},
"reset_water_filter": {
"name": "Reset water filter"
},

View File

@@ -72,6 +72,7 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
def _get_starlink_data(self) -> StarlinkData:
"""Retrieve Starlink data."""
context = self.channel_context
status = status_data(context)
location = location_data(context)
sleep = get_sleep_config(context)
status, obstruction, alert = status_data(context)

View File

@@ -28,7 +28,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import now
from homeassistant.util.variance import ignore_variance
from .coordinator import StarlinkConfigEntry, StarlinkData
from .entity import StarlinkEntity
@@ -92,10 +91,6 @@ class StarlinkAccumulationSensor(StarlinkSensorEntity, RestoreSensor):
self._attr_native_value = last_native_value
uptime_to_stable_datetime = ignore_variance(
lambda value: now() - timedelta(seconds=value), timedelta(minutes=1)
)
SENSORS: tuple[StarlinkSensorEntityDescription, ...] = (
StarlinkSensorEntityDescription(
key="ping",
@@ -155,7 +150,9 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = (
translation_key="last_restart",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: uptime_to_stable_datetime(data.status["uptime"]),
value_fn=lambda data: (
now() - timedelta(seconds=data.status["uptime"], milliseconds=-500)
).replace(microsecond=0),
entity_class=StarlinkSensorEntity,
),
StarlinkSensorEntityDescription(

View File

@@ -28,9 +28,6 @@ from homeassistant.helpers import discovery, issue_registry as ir
from homeassistant.helpers.device import (
async_remove_stale_devices_links_keep_current_device,
)
from homeassistant.helpers.helper_integration import (
async_remove_helper_config_entry_from_source_device,
)
from homeassistant.helpers.reload import async_reload_integration_platforms
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
@@ -119,7 +116,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
# This can be removed in HA Core 2026.7
async_remove_stale_devices_links_keep_current_device(
hass,
entry.entry_id,
@@ -158,41 +154,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1:
if config_entry.minor_version < 2:
# Remove the template config entry from the source device
if source_device_id := config_entry.options.get(CONF_DEVICE_ID):
async_remove_helper_config_entry_from_source_device(
hass,
helper_config_entry_id=config_entry.entry_id,
source_device_id=source_device_id,
)
hass.config_entries.async_update_entry(
config_entry, version=1, minor_version=2
)
_LOGGER.debug(
"Migration to configuration version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
async def _process_config(hass: HomeAssistant, hass_config: ConfigType) -> None:
"""Process config."""
coordinators = hass.data.pop(DATA_COORDINATORS, None)

View File

@@ -697,9 +697,6 @@ class TemplateConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
options_flow = OPTIONS_FLOW
options_flow_reloads = True
MINOR_VERSION = 2
VERSION = 1
@callback
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""

View File

@@ -1,7 +1,7 @@
"""Helpers for template integration."""
from collections.abc import Callable
from enum import StrEnum
from enum import Enum
import hashlib
import itertools
import logging
@@ -33,7 +33,6 @@ from homeassistant.helpers.entity_platform import (
async_get_platforms,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import yaml as yaml_util
@@ -191,12 +190,12 @@ def async_create_template_tracking_entities(
async_add_entities(entities)
def _format_template(value: Any, field: str | None = None) -> Any:
def _format_template(value: Any) -> Any:
if isinstance(value, template.Template):
return value.template
if isinstance(value, StrEnum):
return value.value
if isinstance(value, Enum):
return value.name
if isinstance(value, (int, float, str, bool)):
return value
@@ -208,13 +207,14 @@ def format_migration_config(
config: ConfigType | list[ConfigType], depth: int = 0
) -> ConfigType | list[ConfigType]:
"""Recursive method to format templates as strings from ConfigType."""
types = (dict, list)
if depth > 9:
raise RecursionError
if isinstance(config, list):
items = []
for item in config:
if isinstance(item, (dict, list)):
if isinstance(item, types):
if len(item) > 0:
items.append(format_migration_config(item, depth + 1))
else:
@@ -223,18 +223,9 @@ def format_migration_config(
formatted_config = {}
for field, value in config.items():
if isinstance(value, dict):
if isinstance(value, types):
if len(value) > 0:
formatted_config[field] = format_migration_config(value, depth + 1)
elif isinstance(value, list):
if len(value) > 0:
formatted_config[field] = format_migration_config(value, depth + 1)
else:
formatted_config[field] = []
elif isinstance(value, ScriptVariables):
formatted_config[field] = format_migration_config(
value.as_dict(), depth + 1
)
else:
formatted_config[field] = _format_template(value)

View File

@@ -325,9 +325,6 @@ def async_setup_services(hass: HomeAssistant) -> None:
vol.Required(ATTR_TOU_SETTINGS): dict,
}
),
description_placeholders={
"time_of_use_url": "https://developer.tesla.com/docs/fleet-api#time_of_use_settings"
},
)
async def add_charge_schedule(call: ServiceCall) -> None:

View File

@@ -1358,7 +1358,7 @@
"name": "Energy Site"
},
"tou_settings": {
"description": "See {time_use_url} for details.",
"description": "See https://developer.tesla.com/docs/fleet-api#time_of_use_settings for details.",
"name": "Settings"
}
},

View File

@@ -208,18 +208,6 @@ def _get_temperature_wrappers(
device, DPCode.TEMP_SET_F, prefer_function=True
)
# If there is a temp unit convert dpcode, override empty units
if (
temp_unit_convert := DPCodeEnumWrapper.find_dpcode(
device, DPCode.TEMP_UNIT_CONVERT
)
) is not None:
for wrapper in (temp_current, temp_current_f, temp_set, temp_set_f):
if wrapper is not None and not wrapper.type_information.unit:
wrapper.type_information.unit = temp_unit_convert.read_device_status(
device
)
# Get wrappers for celsius and fahrenheit
# We need to check the unit of measurement
current_celsius = _get_temperature_wrapper(

View File

@@ -35,8 +35,8 @@ from .models import (
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
DPCodeJsonWrapper,
IntegerTypeData,
)
from .type_information import IntegerTypeInformation
from .util import remap_value
@@ -138,24 +138,24 @@ class _ColorTempWrapper(DPCodeIntegerWrapper):
)
DEFAULT_H_TYPE = IntegerTypeInformation(
DEFAULT_H_TYPE = IntegerTypeData(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=360, step=1
)
DEFAULT_S_TYPE = IntegerTypeInformation(
DEFAULT_S_TYPE = IntegerTypeData(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=255, step=1
)
DEFAULT_V_TYPE = IntegerTypeInformation(
DEFAULT_V_TYPE = IntegerTypeData(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=255, step=1
)
DEFAULT_H_TYPE_V2 = IntegerTypeInformation(
DEFAULT_H_TYPE_V2 = IntegerTypeData(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=360, step=1
)
DEFAULT_S_TYPE_V2 = IntegerTypeInformation(
DEFAULT_S_TYPE_V2 = IntegerTypeData(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=1000, step=1
)
DEFAULT_V_TYPE_V2 = IntegerTypeInformation(
DEFAULT_V_TYPE_V2 = IntegerTypeData(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=1000, step=1
)
@@ -578,15 +578,15 @@ def _get_color_data_wrapper(
if function_data := json_loads_object(
cast(str, color_data_wrapper.type_information.type_data)
):
color_data_wrapper.h_type = IntegerTypeInformation(
color_data_wrapper.h_type = IntegerTypeData(
dpcode=color_data_wrapper.dpcode,
**cast(dict, function_data["h"]),
)
color_data_wrapper.s_type = IntegerTypeInformation(
color_data_wrapper.s_type = IntegerTypeData(
dpcode=color_data_wrapper.dpcode,
**cast(dict, function_data["s"]),
)
color_data_wrapper.v_type = IntegerTypeInformation(
color_data_wrapper.v_type = IntegerTypeData(
dpcode=color_data_wrapper.dpcode,
**cast(dict, function_data["v"]),
)

View File

@@ -3,19 +3,15 @@
from __future__ import annotations
import base64
from typing import Any, Self
from dataclasses import dataclass
from typing import Any, Literal, Self, cast, overload
from tuya_sharing import CustomerDevice
from homeassistant.util.json import json_loads
from homeassistant.util.json import json_loads, json_loads_object
from .const import LOGGER, DPType
from .type_information import (
EnumTypeInformation,
IntegerTypeInformation,
TypeInformation,
find_dpcode,
)
from .util import parse_dptype, remap_value
# Dictionary to track logged warnings to avoid spamming logs
# Keyed by device ID
@@ -36,6 +32,139 @@ def _should_log_warning(device_id: str, warning_key: str) -> bool:
return True
@dataclass(kw_only=True)
class TypeInformation:
"""Type information.
As provided by the SDK, from `device.function` / `device.status_range`.
"""
dpcode: str
type_data: str | None = None
@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a TypeInformation object."""
return cls(dpcode=dpcode, type_data=type_data)
@dataclass(kw_only=True)
class IntegerTypeData(TypeInformation):
"""Integer Type Data."""
min: int
max: int
scale: int
step: int
unit: str | None = None
@property
def max_scaled(self) -> float:
"""Return the max scaled."""
return self.scale_value(self.max)
@property
def min_scaled(self) -> float:
"""Return the min scaled."""
return self.scale_value(self.min)
@property
def step_scaled(self) -> float:
"""Return the step scaled."""
return self.step / (10**self.scale)
def scale_value(self, value: int) -> float:
"""Scale a value."""
return value / (10**self.scale)
def scale_value_back(self, value: float) -> int:
"""Return raw value for scaled."""
return round(value * (10**self.scale))
def remap_value_to(
self,
value: float,
to_min: float = 0,
to_max: float = 255,
reverse: bool = False,
) -> float:
"""Remap a value from this range to a new range."""
return remap_value(value, self.min, self.max, to_min, to_max, reverse)
def remap_value_from(
self,
value: float,
from_min: float = 0,
from_max: float = 255,
reverse: bool = False,
) -> float:
"""Remap a value from its current range to this range."""
return remap_value(value, from_min, from_max, self.min, self.max, reverse)
@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a IntegerTypeData object."""
if not (parsed := cast(dict[str, Any] | None, json_loads_object(type_data))):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
min=int(parsed["min"]),
max=int(parsed["max"]),
scale=int(parsed["scale"]),
step=int(parsed["step"]),
unit=parsed.get("unit"),
)
@dataclass(kw_only=True)
class BitmapTypeInformation(TypeInformation):
"""Bitmap type information."""
label: list[str]
@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a BitmapTypeInformation object."""
if not (parsed := json_loads_object(type_data)):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
**cast(dict[str, list[str]], parsed),
)
@dataclass(kw_only=True)
class EnumTypeData(TypeInformation):
"""Enum Type Data."""
range: list[str]
@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a EnumTypeData object."""
if not (parsed := json_loads_object(type_data)):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
**cast(dict[str, list[str]], parsed),
)
_TYPE_INFORMATION_MAPPINGS: dict[DPType, type[TypeInformation]] = {
DPType.BITMAP: BitmapTypeInformation,
DPType.BOOLEAN: TypeInformation,
DPType.ENUM: EnumTypeData,
DPType.INTEGER: IntegerTypeData,
DPType.JSON: TypeInformation,
DPType.RAW: TypeInformation,
DPType.STRING: TypeInformation,
}
class DeviceWrapper:
"""Base device wrapper."""
@@ -174,8 +303,8 @@ class DPCodeJsonWrapper(DPCodeTypeInformationWrapper[TypeInformation]):
return json_loads(raw_value)
class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
"""Simple wrapper for EnumTypeInformation values."""
class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
"""Simple wrapper for EnumTypeData values."""
DPTYPE = DPType.ENUM
@@ -213,12 +342,12 @@ class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
)
class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeInformation]):
"""Simple wrapper for IntegerTypeInformation values."""
class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeData]):
"""Simple wrapper for IntegerTypeData values."""
DPTYPE = DPType.INTEGER
def __init__(self, dpcode: str, type_information: IntegerTypeInformation) -> None:
def __init__(self, dpcode: str, type_information: IntegerTypeData) -> None:
"""Init DPCodeIntegerWrapper."""
super().__init__(dpcode, type_information)
self.native_unit = type_information.unit
@@ -285,3 +414,82 @@ class DPCodeBitmapBitWrapper(DPCodeWrapper):
type_information.dpcode, type_information.label.index(bitmap_key)
)
return None
@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.BITMAP],
) -> BitmapTypeInformation | None: ...
@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.ENUM],
) -> EnumTypeData | None: ...
@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.INTEGER],
) -> IntegerTypeData | None: ...
@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.BOOLEAN, DPType.JSON, DPType.RAW],
) -> TypeInformation | None: ...
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: DPType,
) -> TypeInformation | None:
"""Find type information for a matching DP code available for this device."""
if not (type_information_cls := _TYPE_INFORMATION_MAPPINGS.get(dptype)):
raise NotImplementedError(f"find_dpcode not supported for {dptype}")
if dpcodes is None:
return None
if not isinstance(dpcodes, tuple):
dpcodes = (dpcodes,)
lookup_tuple = (
(device.function, device.status_range)
if prefer_function
else (device.status_range, device.function)
)
for dpcode in dpcodes:
for device_specs in lookup_tuple:
if (
(current_definition := device_specs.get(dpcode))
and parse_dptype(current_definition.type) is dptype
and (
type_information := type_information_cls.from_json(
dpcode=dpcode, type_data=current_definition.values
)
)
):
return type_information
return None

View File

@@ -25,7 +25,7 @@ from .const import (
DPCode,
)
from .entity import TuyaEntity
from .models import DPCodeIntegerWrapper
from .models import DPCodeIntegerWrapper, IntegerTypeData
NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
DeviceCategory.BH: (
@@ -483,6 +483,8 @@ async def async_setup_entry(
class TuyaNumberEntity(TuyaEntity, NumberEntity):
"""Tuya Number Entity."""
_number: IntegerTypeData | None = None
def __init__(
self,
device: CustomerDevice,

View File

@@ -46,12 +46,12 @@ from .models import (
DPCodeJsonWrapper,
DPCodeTypeInformationWrapper,
DPCodeWrapper,
EnumTypeData,
)
from .raw_data_models import ElectricityData
from .type_information import EnumTypeInformation
class _WindDirectionWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
class _WindDirectionWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
"""Custom DPCode Wrapper for converting enum to wind direction."""
DPTYPE = DPType.ENUM

View File

@@ -1,225 +0,0 @@
"""Type information classes for the Tuya integration."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Literal, Self, cast, overload
from tuya_sharing import CustomerDevice
from homeassistant.util.json import json_loads_object
from .const import DPType
from .util import parse_dptype, remap_value
@dataclass(kw_only=True)
class TypeInformation:
"""Type information.
As provided by the SDK, from `device.function` / `device.status_range`.
"""
dpcode: str
type_data: str | None = None
@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a TypeInformation object."""
return cls(dpcode=dpcode, type_data=type_data)
@dataclass(kw_only=True)
class BitmapTypeInformation(TypeInformation):
"""Bitmap type information."""
label: list[str]
@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a BitmapTypeInformation object."""
if not (parsed := json_loads_object(type_data)):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
**cast(dict[str, list[str]], parsed),
)
@dataclass(kw_only=True)
class EnumTypeInformation(TypeInformation):
"""Enum type information."""
range: list[str]
@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return an EnumTypeInformation object."""
if not (parsed := json_loads_object(type_data)):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
**cast(dict[str, list[str]], parsed),
)
@dataclass(kw_only=True)
class IntegerTypeInformation(TypeInformation):
"""Integer type information."""
min: int
max: int
scale: int
step: int
unit: str | None = None
@property
def max_scaled(self) -> float:
"""Return the max scaled."""
return self.scale_value(self.max)
@property
def min_scaled(self) -> float:
"""Return the min scaled."""
return self.scale_value(self.min)
@property
def step_scaled(self) -> float:
"""Return the step scaled."""
return self.step / (10**self.scale)
def scale_value(self, value: int) -> float:
"""Scale a value."""
return value / (10**self.scale)
def scale_value_back(self, value: float) -> int:
"""Return raw value for scaled."""
return round(value * (10**self.scale))
def remap_value_to(
self,
value: float,
to_min: float = 0,
to_max: float = 255,
reverse: bool = False,
) -> float:
"""Remap a value from this range to a new range."""
return remap_value(value, self.min, self.max, to_min, to_max, reverse)
def remap_value_from(
self,
value: float,
from_min: float = 0,
from_max: float = 255,
reverse: bool = False,
) -> float:
"""Remap a value from its current range to this range."""
return remap_value(value, from_min, from_max, self.min, self.max, reverse)
@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return an IntegerTypeInformation object."""
if not (parsed := cast(dict[str, Any] | None, json_loads_object(type_data))):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
min=int(parsed["min"]),
max=int(parsed["max"]),
scale=int(parsed["scale"]),
step=int(parsed["step"]),
unit=parsed.get("unit"),
)
_TYPE_INFORMATION_MAPPINGS: dict[DPType, type[TypeInformation]] = {
DPType.BITMAP: BitmapTypeInformation,
DPType.BOOLEAN: TypeInformation,
DPType.ENUM: EnumTypeInformation,
DPType.INTEGER: IntegerTypeInformation,
DPType.JSON: TypeInformation,
DPType.RAW: TypeInformation,
DPType.STRING: TypeInformation,
}
@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.BITMAP],
) -> BitmapTypeInformation | None: ...
@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.ENUM],
) -> EnumTypeInformation | None: ...
@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.INTEGER],
) -> IntegerTypeInformation | None: ...
@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.BOOLEAN, DPType.JSON, DPType.RAW],
) -> TypeInformation | None: ...
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: DPType,
) -> TypeInformation | None:
"""Find type information for a matching DP code available for this device."""
if not (type_information_cls := _TYPE_INFORMATION_MAPPINGS.get(dptype)):
raise NotImplementedError(f"find_dpcode not supported for {dptype}")
if dpcodes is None:
return None
if not isinstance(dpcodes, tuple):
dpcodes = (dpcodes,)
lookup_tuple = (
(device.function, device.status_range)
if prefer_function
else (device.status_range, device.function)
)
for dpcode in dpcodes:
for device_specs in lookup_tuple:
if (
(current_definition := device_specs.get(dpcode))
and parse_dptype(current_definition.type) is dptype
and (
type_information := type_information_cls.from_json(
dpcode=dpcode, type_data=current_definition.values
)
)
):
return type_information
return None

View File

@@ -64,9 +64,9 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices: list[VeSyncBaseDevice]) -> None:
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)
_setup_entities(devices, async_add_entities)
config_entry.async_on_unload(
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_DEVICES), discover)
@@ -78,11 +78,7 @@ async def async_setup_entry(
@callback
def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities: AddConfigEntryEntitiesCallback,
coordinator: VeSyncDataCoordinator,
) -> None:
def _setup_entities(devices, async_add_entities, coordinator):
"""Add entity."""
async_add_entities(
(

View File

@@ -50,7 +50,7 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices: list[VeSyncBaseDevice]) -> None:
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)
@@ -66,9 +66,9 @@ async def async_setup_entry(
@callback
def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities: AddConfigEntryEntitiesCallback,
async_add_entities,
coordinator: VeSyncDataCoordinator,
) -> None:
):
"""Check if device is fan and add entity."""
async_add_entities(

View File

@@ -53,7 +53,7 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices: list[VeSyncBaseDevice]) -> None:
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)
@@ -73,7 +73,7 @@ def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities: AddConfigEntryEntitiesCallback,
coordinator: VeSyncDataCoordinator,
) -> None:
):
"""Add humidifier entities."""
async_add_entities(VeSyncHumidifierHA(dev, coordinator) for dev in devices)

View File

@@ -38,7 +38,7 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices: list[VeSyncBaseDevice]) -> None:
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)
@@ -54,9 +54,9 @@ async def async_setup_entry(
@callback
def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities: AddConfigEntryEntitiesCallback,
async_add_entities,
coordinator: VeSyncDataCoordinator,
) -> None:
):
"""Check if device is a light and add entity."""
entities: list[VeSyncBaseLightHA] = []
for dev in devices:

View File

@@ -61,7 +61,7 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices: list[VeSyncBaseDevice]) -> None:
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)
@@ -79,7 +79,7 @@ def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities: AddConfigEntryEntitiesCallback,
coordinator: VeSyncDataCoordinator,
) -> None:
):
"""Add number entities."""
async_add_entities(

View File

@@ -115,7 +115,7 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices: list[VeSyncBaseDevice]) -> None:
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)
@@ -133,7 +133,7 @@ def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities: AddConfigEntryEntitiesCallback,
coordinator: VeSyncDataCoordinator,
) -> None:
):
"""Add select entities."""
async_add_entities(

View File

@@ -162,7 +162,7 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices: list[VeSyncBaseDevice]) -> None:
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)
@@ -180,7 +180,7 @@ def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities: AddConfigEntryEntitiesCallback,
coordinator: VeSyncDataCoordinator,
) -> None:
):
"""Check if device is online and add entity."""
async_add_entities(

View File

@@ -77,7 +77,7 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices: list[VeSyncBaseDevice]) -> None:
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)
@@ -93,9 +93,9 @@ async def async_setup_entry(
@callback
def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities: AddConfigEntryEntitiesCallback,
async_add_entities,
coordinator: VeSyncDataCoordinator,
) -> None:
):
"""Check if device is online and add entity."""
async_add_entities(
VeSyncSwitchEntity(dev, description, coordinator)

View File

@@ -22,7 +22,7 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices: list[VeSyncBaseDevice]) -> None:
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)

View File

@@ -4,7 +4,6 @@
"codeowners": ["@ntilley905"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wake_on_lan",
"integration_type": "service",
"iot_class": "local_push",
"requirements": ["wakeonlan==3.1.0"]
}

View File

@@ -1,138 +0,0 @@
"""The WebRTC integration."""
from __future__ import annotations
from collections.abc import Callable, Iterable
from typing import Any
import voluptuous as vol
from webrtc_models import RTCIceServer
from homeassistant.components import websocket_api
from homeassistant.const import CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.core_config import (
CONF_CREDENTIAL,
CONF_ICE_SERVERS,
validate_stun_or_turn_url,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
__all__ = [
"async_get_ice_servers",
"async_register_ice_servers",
]
DOMAIN = "web_rtc"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_ICE_SERVERS): vol.All(
cv.ensure_list,
[
vol.Schema(
{
vol.Required(CONF_URL): vol.All(
cv.ensure_list, [validate_stun_or_turn_url]
),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CREDENTIAL): cv.string,
}
)
],
)
}
)
},
extra=vol.ALLOW_EXTRA,
)
DATA_ICE_SERVERS_USER: HassKey[Iterable[RTCIceServer]] = HassKey(
"web_rtc_ice_servers_user"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"web_rtc_ice_servers"
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the WebRTC integration."""
servers = [
RTCIceServer(
server[CONF_URL],
server.get(CONF_USERNAME),
server.get(CONF_CREDENTIAL),
)
for server in config.get(DOMAIN, {}).get(CONF_ICE_SERVERS, [])
]
if servers:
hass.data[DATA_ICE_SERVERS_USER] = servers
hass.data[DATA_ICE_SERVERS] = []
websocket_api.async_register_command(hass, ws_ice_servers)
return True
@callback
def async_register_ice_servers(
hass: HomeAssistant,
get_ice_server_fn: Callable[[], Iterable[RTCIceServer]],
) -> Callable[[], None]:
"""Register an ICE server.
The registering integration is responsible to implement caching if needed.
"""
servers = hass.data[DATA_ICE_SERVERS]
def remove() -> None:
servers.remove(get_ice_server_fn)
servers.append(get_ice_server_fn)
return remove
@callback
def async_get_ice_servers(hass: HomeAssistant) -> list[RTCIceServer]:
"""Return all registered ICE servers."""
servers: list[RTCIceServer] = []
if hass.config.webrtc.ice_servers:
servers.extend(hass.config.webrtc.ice_servers)
if DATA_ICE_SERVERS_USER in hass.data:
servers.extend(hass.data[DATA_ICE_SERVERS_USER])
if not servers:
servers = [
RTCIceServer(
urls=[
"stun:stun.home-assistant.io:3478",
"stun:stun.home-assistant.io:80",
]
),
]
for gen_servers in hass.data[DATA_ICE_SERVERS]:
servers.extend(gen_servers())
return servers
@websocket_api.websocket_command(
{
"type": "web_rtc/ice_servers",
}
)
@callback
def ws_ice_servers(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Handle get WebRTC ICE servers websocket command."""
ice_servers = [server.to_dict() for server in async_get_ice_servers(hass)]
connection.send_result(msg["id"], ice_servers)

View File

@@ -1,8 +0,0 @@
{
"domain": "web_rtc",
"name": "WebRTC",
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/web_rtc",
"integration_type": "system",
"quality_scale": "internal"
}

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from typing import Any, Self
@@ -25,18 +24,9 @@ from homeassistant.helpers.trigger import (
async_get_all_descriptions as async_get_all_trigger_descriptions,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
FLATTENED_SERVICE_DESCRIPTIONS_CACHE: HassKey[
tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]
] = HassKey("websocket_automation_flat_service_description_cache")
AUTOMATION_COMPONENT_LOOKUP_CACHE: HassKey[
dict[str, tuple[Mapping[str, Any], _AutomationComponentLookupTable]]
] = HassKey("websocket_automation_component_lookup_cache")
@dataclass(slots=True, kw_only=True)
class _EntityFilter:
@@ -111,14 +101,6 @@ class _AutomationComponentLookupData:
)
@dataclass(slots=True, kw_only=True)
class _AutomationComponentLookupTable:
"""Helper class for looking up automation components."""
domain_components: dict[str | None, list[_AutomationComponentLookupData]]
component_count: int
def _get_automation_component_domains(
target_description: dict[str, Any],
) -> set[str | None]:
@@ -150,54 +132,11 @@ def _get_automation_component_domains(
return domains
def _get_automation_component_lookup_table(
hass: HomeAssistant,
component_type: str,
component_descriptions: Mapping[str, Mapping[str, Any] | None],
) -> _AutomationComponentLookupTable:
"""Get a dict of automation components keyed by domain, along with the total number of components.
Returns a cached object if available.
"""
try:
cache = hass.data[AUTOMATION_COMPONENT_LOOKUP_CACHE]
except KeyError:
cache = hass.data[AUTOMATION_COMPONENT_LOOKUP_CACHE] = {}
if (cached := cache.get(component_type)) is not None:
cached_descriptions, cached_lookup = cached
if cached_descriptions is component_descriptions:
_LOGGER.debug(
"Using cached automation component lookup data for %s", component_type
)
return cached_lookup
lookup_table = _AutomationComponentLookupTable(
domain_components={}, component_count=0
)
for component, description in component_descriptions.items():
if description is None or CONF_TARGET not in description:
_LOGGER.debug("Skipping component %s without target description", component)
continue
domains = _get_automation_component_domains(description[CONF_TARGET])
lookup_data = _AutomationComponentLookupData.create(
component, description[CONF_TARGET]
)
for domain in domains:
lookup_table.domain_components.setdefault(domain, []).append(lookup_data)
lookup_table.component_count += 1
cache[component_type] = (component_descriptions, lookup_table)
return lookup_table
def _async_get_automation_components_for_target(
hass: HomeAssistant,
component_type: str,
target_selection: ConfigType,
expand_group: bool,
component_descriptions: Mapping[str, Mapping[str, Any] | None],
component_descriptions: dict[str, dict[str, Any] | None],
) -> set[str]:
"""Get automation components (triggers/conditions/services) for a target.
@@ -210,17 +149,27 @@ def _async_get_automation_components_for_target(
)
_LOGGER.debug("Extracted entities for lookup: %s", extracted)
lookup_table = _get_automation_component_lookup_table(
hass, component_type, component_descriptions
)
_LOGGER.debug(
"Automation components per domain: %s", lookup_table.domain_components
)
# Build lookup structure: domain -> list of trigger/condition/service lookup data
domain_components: dict[str | None, list[_AutomationComponentLookupData]] = {}
component_count = 0
for component, description in component_descriptions.items():
if description is None or CONF_TARGET not in description:
_LOGGER.debug("Skipping component %s without target description", component)
continue
domains = _get_automation_component_domains(description[CONF_TARGET])
lookup_data = _AutomationComponentLookupData.create(
component, description[CONF_TARGET]
)
for domain in domains:
domain_components.setdefault(domain, []).append(lookup_data)
component_count += 1
_LOGGER.debug("Automation components per domain: %s", domain_components)
entity_infos = entity_sources(hass)
matched_components: set[str] = set()
for entity_id in extracted.referenced | extracted.indirectly_referenced:
if lookup_table.component_count == len(matched_components):
if component_count == len(matched_components):
# All automation components matched already, so we don't need to iterate further
break
@@ -232,11 +181,7 @@ def _async_get_automation_components_for_target(
entity_domain = entity_id.split(".")[0]
entity_integration = entity_info["domain"]
for domain in (entity_domain, entity_integration, None):
if not (
domain_component_data := lookup_table.domain_components.get(domain)
):
continue
for component_data in domain_component_data:
for component_data in domain_components.get(domain, []):
if component_data.component in matched_components:
continue
if component_data.matches(
@@ -253,7 +198,7 @@ async def async_get_triggers_for_target(
"""Get triggers for a target."""
descriptions = await async_get_all_trigger_descriptions(hass)
return _async_get_automation_components_for_target(
hass, "triggers", target_selector, expand_group, descriptions
hass, target_selector, expand_group, descriptions
)
@@ -263,7 +208,7 @@ async def async_get_conditions_for_target(
"""Get conditions for a target."""
descriptions = await async_get_all_condition_descriptions(hass)
return _async_get_automation_components_for_target(
hass, "conditions", target_selector, expand_group, descriptions
hass, target_selector, expand_group, descriptions
)
@@ -272,33 +217,12 @@ async def async_get_services_for_target(
) -> set[str]:
"""Get services for a target."""
descriptions = await async_get_all_service_descriptions(hass)
def get_flattened_service_descriptions() -> dict[str, dict[str, Any]]:
"""Get flattened service descriptions, with caching."""
if FLATTENED_SERVICE_DESCRIPTIONS_CACHE in hass.data:
cached_descriptions, cached_flattened_descriptions = hass.data[
FLATTENED_SERVICE_DESCRIPTIONS_CACHE
]
# If the descriptions are the same, return the cached flattened version
if cached_descriptions is descriptions:
return cached_flattened_descriptions
# Flatten dicts to be keyed by domain.name to match trigger/condition format
flattened_descriptions = {
f"{domain}.{service_name}": desc
for domain, services in descriptions.items()
for service_name, desc in services.items()
}
hass.data[FLATTENED_SERVICE_DESCRIPTIONS_CACHE] = (
descriptions,
flattened_descriptions,
)
return flattened_descriptions
# Flatten dicts to be keyed by domain.name to match trigger/condition format
descriptions_flatten = {
f"{domain}.{service_name}": desc
for domain, services in descriptions.items()
for service_name, desc in services.items()
}
return _async_get_automation_components_for_target(
hass,
"services",
target_selector,
expand_group,
get_flattened_service_descriptions(),
hass, target_selector, expand_group, descriptions_flatten
)

View File

@@ -19,8 +19,6 @@ from pythonxbox.api.provider.smartglass.models import (
from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType
from .entity import to_https
class MediaTypeDetails(NamedTuple):
"""Details for media type."""
@@ -153,5 +151,5 @@ def _find_media_image(images: list[Image]) -> str | None:
if match := next(
(image for image in images if image.image_purpose == purpose), None
):
return to_https(match.uri)
return f"https:{match.uri}" if match.uri.startswith("/") else match.uri
return None

View File

@@ -151,15 +151,6 @@ def check_deprecated_entity(
return False
def to_https(image_url: str) -> str:
"""Convert image URLs to secure URLs."""
url = URL(image_url)
if url.host == "images-eds.xboxlive.com":
url = url.with_host("images-eds-ssl.xboxlive.com")
return str(url.with_scheme("https"))
def profile_pic(person: Person, _: Title | None = None) -> str | None:
"""Return the gamer pic."""
@@ -169,4 +160,9 @@ def profile_pic(person: Person, _: Title | None = None) -> str | None:
# to point to the correct image, with the correct domain and certificate.
# We need to also remove the 'mode=Padding' query because with it,
# it results in an error 400.
return str(URL(to_https(person.display_pic_raw)).without_query_params("mode"))
url = URL(person.display_pic_raw)
if url.host == "images-eds.xboxlive.com":
url = url.with_host("images-eds-ssl.xboxlive.com").with_scheme("https")
query = dict(url.query)
query.pop("mode", None)
return str(url.with_query(query))

View File

@@ -22,7 +22,6 @@ from homeassistant.util import dt as dt_util
from .binary_sensor import profile_pic
from .const import DOMAIN
from .coordinator import XboxConfigEntry
from .entity import to_https
_LOGGER = logging.getLogger(__name__)
@@ -656,6 +655,6 @@ def game_thumbnail(images: list[Image]) -> str | None:
(i for i in images if i.type == img_type),
None,
):
return to_https(match.url)
return match.url
return None

View File

@@ -34,7 +34,6 @@ from .entity import (
XboxBaseEntity,
XboxBaseEntityDescription,
check_deprecated_entity,
to_https,
)
PARALLEL_UPDATES = 0
@@ -143,8 +142,8 @@ def title_logo(_: Person, title: Title | None) -> str | None:
"""Get the game logo."""
return (
next((to_https(i.url) for i in title.images if i.type == "Tile"), None)
or next((to_https(i.url) for i in title.images if i.type == "Logo"), None)
next((i.url for i in title.images if i.type == "Tile"), None)
or next((i.url for i in title.images if i.type == "Logo"), None)
if title and title.images
else None
)

View File

@@ -44,11 +44,6 @@ GATEWAY_SETTINGS = vol.Schema(
}
)
ERROR_STEP_PLACEHOLDERS = {
"tutorial_url": "https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz",
"invalid_host_url": "https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
}
class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Xiaomi Aqara config flow."""
@@ -71,12 +66,7 @@ class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN):
if (self.host is None and self.sid is None) or errors:
schema = GATEWAY_CONFIG_HOST
return self.async_show_form(
step_id="user",
data_schema=schema,
errors=errors,
description_placeholders=ERROR_STEP_PLACEHOLDERS,
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -159,10 +149,7 @@ class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
step_id="select",
data_schema=select_schema,
errors=errors,
description_placeholders=ERROR_STEP_PLACEHOLDERS,
step_id="select", data_schema=select_schema, errors=errors
)
async def async_step_zeroconf(
@@ -249,8 +236,5 @@ class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN):
errors[CONF_KEY] = "invalid_key"
return self.async_show_form(
step_id="settings",
data_schema=GATEWAY_SETTINGS,
errors=errors,
description_placeholders=ERROR_STEP_PLACEHOLDERS,
step_id="settings", data_schema=GATEWAY_SETTINGS, errors=errors
)

View File

@@ -7,7 +7,7 @@
},
"error": {
"discovery_error": "Failed to discover a Xiaomi Aqara Gateway, try using the IP of the device running Home Assistant as interface",
"invalid_host": "Invalid hostname or IP address, see {invalid_host_url}",
"invalid_host": "Invalid hostname or IP address, see https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
"invalid_interface": "Invalid network interface",
"invalid_key": "Invalid Gateway key",
"invalid_mac": "Invalid MAC address"
@@ -25,7 +25,7 @@
"key": "The key of your Gateway",
"name": "Name of the Gateway"
},
"description": "The key (password) can be retrieved using this tutorial: {tutorial_url}. If the key is not provided only sensors will be accessible",
"description": "The key (password) can be retrieved using this tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. If the key is not provided only sensors will be accessible",
"title": "Optional settings"
},
"user": {

View File

@@ -380,12 +380,7 @@ def _async_setup_services(hass: HomeAssistant):
SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "async_set_mode"
)
platform.async_register_entity_service(
SERVICE_START_FLOW,
SERVICE_SCHEMA_START_FLOW,
_async_start_flow,
description_placeholders={
"flow_objects_urls": "https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects"
},
SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow
)
platform.async_register_entity_service(
SERVICE_SET_COLOR_SCENE, SERVICE_SCHEMA_SET_COLOR_SCENE, _async_set_color_scene
@@ -402,9 +397,6 @@ def _async_setup_services(hass: HomeAssistant):
SERVICE_SET_COLOR_FLOW_SCENE,
SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE,
_async_set_color_flow_scene,
description_placeholders={
"examples_url": "https://yeelight.readthedocs.io/en/stable/flow.html"
},
)
platform.async_register_entity_service(
SERVICE_SET_AUTO_DELAY_OFF_SCENE,

View File

@@ -102,7 +102,7 @@
"name": "Count"
},
"transitions": {
"description": "Array of transitions, for desired effect. Examples {examples_url}.",
"description": "Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html.",
"name": "Transitions"
}
},
@@ -171,7 +171,7 @@
"name": "Set music mode"
},
"start_flow": {
"description": "Starts a custom flow, using transitions from {flow_objects_urls}.",
"description": "Starts a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects.",
"fields": {
"action": {
"description": "[%key:component::yeelight::services::set_color_flow_scene::fields::action::description%]",

View File

@@ -680,13 +680,6 @@ class BaseZhaFlow(ConfigEntryBaseFlow):
try:
await self._form_network_task
except Exception as exc:
_LOGGER.exception("Failed to form new network")
self._progress_error = AbortFlow(
reason="cannot_form_network",
description_placeholders={"error": str(exc)},
)
return self.async_show_progress_done(next_step_id="progress_failed")
finally:
self._form_network_task = None

View File

@@ -1,7 +1,6 @@
{
"config": {
"abort": {
"cannot_form_network": "Could not form a new Zigbee network.\n\nError: {error}",
"cannot_resolve_path": "Could not resolve device path: {path}",
"cannot_restore_backup": "The adapter you are restoring to does not properly support backup restoration. Please upgrade the firmware.\n\nError: {error}",
"cannot_restore_backup_no_ieee_confirm": "The adapter you are restoring to has outdated firmware and cannot write the adapter IEEE address multiple times. Please upgrade the firmware or confirm permanent overwrite in the previous step.",
@@ -1924,7 +1923,6 @@
},
"options": {
"abort": {
"cannot_form_network": "[%key:component::zha::config::abort::cannot_form_network%]",
"cannot_resolve_path": "[%key:component::zha::config::abort::cannot_resolve_path%]",
"cannot_restore_backup": "[%key:component::zha::config::abort::cannot_restore_backup%]",
"cannot_restore_backup_no_ieee_confirm": "[%key:component::zha::config::abort::cannot_restore_backup_no_ieee_confirm%]",

View File

@@ -306,9 +306,6 @@ class ZWaveServices:
has_at_least_one_node,
),
),
description_placeholders={
"api_docs_url": "https://zwave-js.github.io/node-zwave-js/#/api/CCs/index"
},
)
self._hass.services.async_register(

View File

@@ -400,11 +400,11 @@
"name": "Entity ID(s)"
},
"method_name": {
"description": "The name of the API method to call. Refer to the Z-Wave Command Class API documentation ({api_docs_url}) for available methods.",
"description": "The name of the API method to call. Refer to the Z-Wave Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for available methods.",
"name": "Method name"
},
"parameters": {
"description": "A list of parameters to pass to the API method. Refer to the Z-Wave Command Class API documentation ({api_docs_url}) for parameters.",
"description": "A list of parameters to pass to the API method. Refer to the Z-Wave Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters.",
"name": "Parameters"
}
},

View File

@@ -3384,7 +3384,10 @@ class ConfigFlow(ConfigEntryBaseFlow):
last_step: bool | None = None,
preview: str | None = None,
) -> ConfigFlowResult:
"""Return the definition of a form to gather user input."""
"""Return the definition of a form to gather user input.
The step_id parameter is deprecated and will be removed in a future release.
"""
if self.source == SOURCE_REAUTH and "entry_id" in self.context:
# If the integration does not provide a name for the reauth title,
# we append it to the description placeholders.

View File

@@ -249,7 +249,7 @@ def _validate_currency(data: Any) -> Any:
raise
def validate_stun_or_turn_url(value: Any) -> str:
def _validate_stun_or_turn_url(value: Any) -> str:
"""Validate an URL."""
url_in = str(value)
url = urlparse(url_in)
@@ -331,7 +331,7 @@ CORE_CONFIG_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_URL): vol.All(
cv.ensure_list, [validate_stun_or_turn_url]
cv.ensure_list, [_validate_stun_or_turn_url]
),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CREDENTIAL): cv.string,

View File

@@ -9,7 +9,6 @@ APPLICATION_CREDENTIALS = [
"ekeybionyx",
"electric_kiwi",
"fitbit",
"gentex_homelink",
"geocaching",
"google",
"google_assistant_sdk",

Some files were not shown because too many files have changed in this diff Show More