mirror of
https://github.com/home-assistant/core.git
synced 2026-05-21 16:00:12 +01:00
Fix line length violations in components s (#170722)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: frenck <195327+frenck@users.noreply.github.com>
This commit is contained in:
@@ -37,7 +37,8 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity)
|
||||
config_entry = coordinator.config_entry
|
||||
self._mac: str | None = config_entry.data.get(CONF_MAC)
|
||||
self._host: str | None = config_entry.data.get(CONF_HOST)
|
||||
# Fallback for legacy models that doesn't have a API to retrieve MAC or SerialNumber
|
||||
# Fallback for legacy models that doesn't have a API
|
||||
# to retrieve MAC or SerialNumber
|
||||
self._attr_unique_id = config_entry.unique_id or config_entry.entry_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
manufacturer=config_entry.data.get(CONF_MANUFACTURER),
|
||||
|
||||
@@ -135,7 +135,8 @@ async def async_migrate_entry(
|
||||
SUBENTRY_TYPE_SWITCHABLE_OUTPUT: CONF_SWITCHABLE_OUTPUT_NUMBER,
|
||||
}
|
||||
|
||||
new_title = f"{subentry.title} ({subentry.data[property_map[subentry.subentry_type]]})"
|
||||
prop = property_map[subentry.subentry_type]
|
||||
new_title = f"{subentry.title} ({subentry.data[prop]})"
|
||||
|
||||
hass.config_entries.async_update_subentry(
|
||||
config_entry, subentry, title=new_title
|
||||
@@ -143,7 +144,8 @@ async def async_migrate_entry(
|
||||
|
||||
hass.config_entries.async_update_entry(config_entry, minor_version=2)
|
||||
|
||||
# 2.1 Migrate all entity unique IDs to replace "satel" prefix with config entry ID, allows multiple entries to be configured
|
||||
# 2.1 Migrate all entity unique IDs to replace "satel" prefix
|
||||
# with config entry ID, allows multiple entries to be configured
|
||||
if config_entry.version == 1:
|
||||
|
||||
@callback
|
||||
|
||||
@@ -41,7 +41,8 @@ class SatelClient:
|
||||
host = entry.data[CONF_HOST]
|
||||
port = entry.data[CONF_PORT]
|
||||
|
||||
# Make sure we initialize the Satel controller with the configured entries to monitor
|
||||
# Make sure we initialize the Satel controller
|
||||
# with the configured entries to monitor
|
||||
partitions = [
|
||||
subentry.data[CONF_PARTITION_NUMBER]
|
||||
for subentry in entry.subentries.values()
|
||||
|
||||
@@ -312,7 +312,9 @@ class PartitionSubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"{user_input[CONF_NAME]} ({user_input[CONF_PARTITION_NUMBER]})",
|
||||
title=(
|
||||
f"{user_input[CONF_NAME]} ({user_input[CONF_PARTITION_NUMBER]})"
|
||||
),
|
||||
data=user_input,
|
||||
unique_id=unique_id,
|
||||
)
|
||||
@@ -339,7 +341,10 @@ class PartitionSubentryFlowHandler(ConfigSubentryFlow):
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
subconfig_entry,
|
||||
title=f"{user_input[CONF_NAME]} ({subconfig_entry.data[CONF_PARTITION_NUMBER]})",
|
||||
title=(
|
||||
f"{user_input[CONF_NAME]}"
|
||||
f" ({subconfig_entry.data[CONF_PARTITION_NUMBER]})"
|
||||
),
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
||||
@@ -425,7 +430,10 @@ class ZoneSubentryFlowHandler(ConfigSubentryFlow):
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
subconfig_entry,
|
||||
title=f"{user_input[CONF_NAME]} ({subconfig_entry.data[CONF_ZONE_NUMBER]})",
|
||||
title=(
|
||||
f"{user_input[CONF_NAME]}"
|
||||
f" ({subconfig_entry.data[CONF_ZONE_NUMBER]})"
|
||||
),
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
||||
@@ -491,7 +499,10 @@ class OutputSubentryFlowHandler(ConfigSubentryFlow):
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
subconfig_entry,
|
||||
title=f"{user_input[CONF_NAME]} ({subconfig_entry.data[CONF_OUTPUT_NUMBER]})",
|
||||
title=(
|
||||
f"{user_input[CONF_NAME]}"
|
||||
f" ({subconfig_entry.data[CONF_OUTPUT_NUMBER]})"
|
||||
),
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
||||
@@ -516,7 +527,10 @@ class SwitchableOutputSubentryFlowHandler(ConfigSubentryFlow):
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
unique_id = f"{SUBENTRY_TYPE_SWITCHABLE_OUTPUT}_{user_input[CONF_SWITCHABLE_OUTPUT_NUMBER]}"
|
||||
unique_id = (
|
||||
f"{SUBENTRY_TYPE_SWITCHABLE_OUTPUT}"
|
||||
f"_{user_input[CONF_SWITCHABLE_OUTPUT_NUMBER]}"
|
||||
)
|
||||
|
||||
for existing_subentry in self._get_entry().subentries.values():
|
||||
if existing_subentry.unique_id == unique_id:
|
||||
@@ -524,7 +538,10 @@ class SwitchableOutputSubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"{user_input[CONF_NAME]} ({user_input[CONF_SWITCHABLE_OUTPUT_NUMBER]})",
|
||||
title=(
|
||||
f"{user_input[CONF_NAME]}"
|
||||
f" ({user_input[CONF_SWITCHABLE_OUTPUT_NUMBER]})"
|
||||
),
|
||||
data=user_input,
|
||||
unique_id=unique_id,
|
||||
)
|
||||
@@ -551,7 +568,10 @@ class SwitchableOutputSubentryFlowHandler(ConfigSubentryFlow):
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
subconfig_entry,
|
||||
title=f"{user_input[CONF_NAME]} ({subconfig_entry.data[CONF_SWITCHABLE_OUTPUT_NUMBER]})",
|
||||
title=(
|
||||
f"{user_input[CONF_NAME]}"
|
||||
f" ({subconfig_entry.data[CONF_SWITCHABLE_OUTPUT_NUMBER]})"
|
||||
),
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
||||
|
||||
@@ -68,7 +68,8 @@ def valid_schedule(schedule: list[dict[str, str]]) -> list[dict[str, str]]:
|
||||
# Sort the schedule by start times
|
||||
schedule = sorted(schedule, key=lambda time_range: time_range[CONF_FROM])
|
||||
|
||||
# Check if the start time of the next event is before the end time of the previous event
|
||||
# Check if the start time of the next event is before
|
||||
# the end time of the previous event
|
||||
previous_to = None
|
||||
for time_range in schedule:
|
||||
if time_range[CONF_FROM] >= time_range[CONF_TO]:
|
||||
@@ -269,7 +270,8 @@ class Schedule(CollectionEntity):
|
||||
self._attr_name = self._config[CONF_NAME]
|
||||
self._attr_unique_id = self._config[CONF_ID]
|
||||
|
||||
# Exclude any custom attributes that may be present on time ranges from recording.
|
||||
# Exclude any custom attributes that may be present
|
||||
# on time ranges from recording.
|
||||
self._unrecorded_attributes = self.all_custom_data_keys()
|
||||
self._Entity__combined_unrecorded_attributes = (
|
||||
self._entity_component_unrecorded_attributes | self._unrecorded_attributes
|
||||
|
||||
@@ -190,7 +190,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ScrapeConfigEntry) ->
|
||||
unique_id=None,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Migrating sensor %s with unique id %s to sub config entry id %s, old data %s, new data %s",
|
||||
"Migrating sensor %s with unique id %s to sub config"
|
||||
" entry id %s, old data %s, new data %s",
|
||||
title,
|
||||
old_unique_id,
|
||||
new_sub_entry.subentry_id,
|
||||
|
||||
@@ -59,7 +59,8 @@ class ScrapeCoordinator(DataUpdateCoordinator[BeautifulSoup]):
|
||||
raise UpdateFailed("REST data is not available")
|
||||
|
||||
# Detect if content is XML and use appropriate parser
|
||||
# Check Content-Type header first (most reliable), then fall back to content detection
|
||||
# Check Content-Type header first (most reliable),
|
||||
# then fall back to content detection
|
||||
parser = "lxml"
|
||||
headers = self._rest.headers
|
||||
content_type = headers.get("Content-Type", "") if headers else ""
|
||||
@@ -76,7 +77,8 @@ class ScrapeCoordinator(DataUpdateCoordinator[BeautifulSoup]):
|
||||
after_xml_lower = after_xml.lower()
|
||||
is_html = after_xml_lower.startswith(("<!doctype html", "<html"))
|
||||
if is_html:
|
||||
# Strip XML declaration from HTML to avoid XMLParsedAsHTMLWarning
|
||||
# Strip XML declaration from HTML
|
||||
# to avoid XMLParsedAsHTMLWarning
|
||||
data = after_xml
|
||||
else:
|
||||
parser = "lxml-xml"
|
||||
|
||||
@@ -145,7 +145,8 @@ async def _async_migrate_entries(
|
||||
continue
|
||||
if device == "pump" and source_index is None:
|
||||
_LOGGER.debug(
|
||||
"Unable to parse 'source_index' from existing unique_id for pump entity '%s'",
|
||||
"Unable to parse 'source_index' from existing"
|
||||
" unique_id for pump entity '%s'",
|
||||
source_key,
|
||||
)
|
||||
continue
|
||||
@@ -160,7 +161,8 @@ async def _async_migrate_entries(
|
||||
entry.domain, entry.platform, new_unique_id
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Cannot migrate '%s' to unique_id '%s', already exists for entity '%s'. Aborting",
|
||||
"Cannot migrate '%s' to unique_id '%s',"
|
||||
" already exists for entity '%s'. Aborting",
|
||||
entry.unique_id,
|
||||
new_unique_id,
|
||||
existing_entity_id,
|
||||
|
||||
@@ -138,7 +138,8 @@ class ScreenLogicPushEntity(ScreenLogicEntity):
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
# For push entities, only take updates from the coordinator if availability changes.
|
||||
# For push entities, only take updates from the
|
||||
# coordinator if availability changes.
|
||||
if self.coordinator.last_update_success != self._last_update_success:
|
||||
self._async_data_updated()
|
||||
|
||||
|
||||
@@ -135,7 +135,8 @@ class Searcher:
|
||||
# Scripts referencing this area
|
||||
self._add(ItemType.SCRIPT, script.scripts_with_area(self.hass, area_id))
|
||||
|
||||
# Entity in this area, will extend this with the entities of the devices in this area
|
||||
# Entity in this area, will extend this with
|
||||
# the entities of the devices in this area
|
||||
entity_entries = er.async_entries_for_area(self._entity_registry, area_id)
|
||||
|
||||
# Devices in this area
|
||||
|
||||
@@ -300,7 +300,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
await super().async_internal_added_to_hass()
|
||||
if self.entity_category == EntityCategory.CONFIG:
|
||||
raise HomeAssistantError(
|
||||
f"Entity {self.entity_id} cannot be added as the entity category is set to config"
|
||||
f"Entity {self.entity_id} cannot be added as"
|
||||
" the entity category is set to config"
|
||||
)
|
||||
|
||||
if not self.registry_entry:
|
||||
@@ -417,8 +418,10 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if suggested_unit_of_measurement is None and (
|
||||
unit_converter := UNIT_CONVERTERS.get(self.device_class)
|
||||
):
|
||||
# If the device class is not known by the unit system but has a unit converter,
|
||||
# fall back to the unit suggested by the unit converter's unit class.
|
||||
# If the device class is not known by the unit
|
||||
# system but has a unit converter, fall back to
|
||||
# the unit suggested by the unit converter's
|
||||
# unit class.
|
||||
suggested_unit_of_measurement = self.hass.config.units.get_converted_unit(
|
||||
unit_converter.UNIT_CLASS, self.__native_unit_of_measurement_compat
|
||||
)
|
||||
@@ -458,9 +461,12 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
state_class = self.state_class
|
||||
if state_class != SensorStateClass.TOTAL:
|
||||
raise ValueError(
|
||||
f"Entity {self.entity_id} ({type(self)}) with state_class {state_class}"
|
||||
" has set last_reset. Setting last_reset for entities with state_class"
|
||||
" other than 'total' is not supported. Please update your configuration"
|
||||
f"Entity {self.entity_id} ({type(self)})"
|
||||
f" with state_class {state_class}"
|
||||
" has set last_reset. Setting last_reset"
|
||||
" for entities with state_class"
|
||||
" other than 'total' is not supported."
|
||||
" Please update your configuration"
|
||||
" if state_class is manually configured."
|
||||
)
|
||||
|
||||
@@ -567,9 +573,13 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
):
|
||||
if native_unit_of_measurement is not None:
|
||||
raise ValueError(
|
||||
f"Sensor {type(self)} from integration '{self.platform.platform_name}' "
|
||||
f"has a translation key for unit_of_measurement '{unit_of_measurement}', "
|
||||
f"but also has a native_unit_of_measurement '{native_unit_of_measurement}'"
|
||||
f"Sensor {type(self)} from integration"
|
||||
f" '{self.platform.platform_name}' "
|
||||
"has a translation key for"
|
||||
f" unit_of_measurement '{unit_of_measurement}'"
|
||||
", but also has a"
|
||||
" native_unit_of_measurement"
|
||||
f" '{native_unit_of_measurement}'"
|
||||
)
|
||||
return unit_of_measurement
|
||||
|
||||
@@ -654,8 +664,10 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
return value.isoformat(timespec="seconds")
|
||||
except (AttributeError, OverflowError, TypeError) as err:
|
||||
raise ValueError(
|
||||
f"Invalid datetime: {self.entity_id} has {device_class.value} device class "
|
||||
f"but provides state {value}:{type(value)} resulting in '{err}'"
|
||||
f"Invalid datetime: {self.entity_id}"
|
||||
f" has {device_class.value} device class"
|
||||
f" but provides state {value}:{type(value)}"
|
||||
f" resulting in '{err}'"
|
||||
) from err
|
||||
|
||||
# Received a date value
|
||||
@@ -773,9 +785,12 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
and native_unit_of_measurement not in units
|
||||
):
|
||||
raise ValueError(
|
||||
f"Sensor {self.entity_id} ({type(self)}) is using native unit of "
|
||||
f"measurement '{native_unit_of_measurement}' which is not a valid unit "
|
||||
f"for the state class ('{state_class}') it is using; expected one of {units};"
|
||||
f"Sensor {self.entity_id} ({type(self)}) is"
|
||||
" using native unit of measurement"
|
||||
f" '{native_unit_of_measurement}' which is"
|
||||
" not a valid unit for the state class"
|
||||
f" ('{state_class}') it is using;"
|
||||
f" expected one of {units};"
|
||||
)
|
||||
|
||||
return value
|
||||
@@ -794,15 +809,18 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def _get_adjusted_display_precision(self) -> int | None:
|
||||
"""Return the display precision for the sensor.
|
||||
|
||||
When the integration has specified a suggested display precision, it will be used.
|
||||
If a unit conversion is needed, the display precision will be adjusted based on
|
||||
the ratio from the native unit to the current one.
|
||||
When the integration has specified a suggested display
|
||||
precision, it will be used. If a unit conversion is needed,
|
||||
the display precision will be adjusted based on the ratio
|
||||
from the native unit to the current one.
|
||||
|
||||
When the integration does not specify a suggested display precision, a default
|
||||
device class precision will be used from UNITS_PRECISION, and the final precision
|
||||
will be adjusted based on the ratio from the default unit to the current one. It
|
||||
will also be capped so that the extra precision (from the base unit) does not
|
||||
exceed DEFAULT_PRECISION_LIMIT.
|
||||
When the integration does not specify a suggested
|
||||
display precision, a default device class precision will
|
||||
be used from UNITS_PRECISION, and the final precision
|
||||
will be adjusted based on the ratio from the default
|
||||
unit to the current one. It will also be capped so that
|
||||
the extra precision (from the base unit) does not exceed
|
||||
DEFAULT_PRECISION_LIMIT.
|
||||
"""
|
||||
display_precision = self.suggested_display_precision
|
||||
device_class = self.device_class
|
||||
|
||||
@@ -175,7 +175,8 @@ class SensorDeviceClass(StrEnum):
|
||||
CO = "carbon_monoxide"
|
||||
"""Carbon Monoxide gas concentration.
|
||||
|
||||
Unit of measurement: `ppb` (parts per billion), `ppm` (parts per million), `mg/m³`, `μg/m³`
|
||||
Unit of measurement: `ppb` (parts per billion),
|
||||
`ppm` (parts per million), `mg/m³`, `μg/m³`
|
||||
"""
|
||||
|
||||
CO2 = "carbon_dioxide"
|
||||
@@ -227,16 +228,20 @@ class SensorDeviceClass(StrEnum):
|
||||
|
||||
Use this device class for sensors measuring energy consumption, for example
|
||||
electric energy consumption.
|
||||
Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `mWh`, `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `cal`, `kcal`, `Mcal`, `Gcal`
|
||||
Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `mWh`,
|
||||
`Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `cal`, `kcal`,
|
||||
`Mcal`, `Gcal`
|
||||
"""
|
||||
|
||||
ENERGY_DISTANCE = "energy_distance"
|
||||
"""Energy distance.
|
||||
|
||||
Use this device class for sensors measuring energy by distance, for example the amount
|
||||
of electric energy consumed by an electric car.
|
||||
Use this device class for sensors measuring energy by
|
||||
distance, for example the amount of electric energy
|
||||
consumed by an electric car.
|
||||
|
||||
Unit of measurement: `kWh/100km`, `Wh/km`, `mi/kWh`, `km/kWh`
|
||||
Unit of measurement: `kWh/100km`, `Wh/km`,
|
||||
`mi/kWh`, `km/kWh`
|
||||
"""
|
||||
|
||||
ENERGY_STORAGE = "energy_storage"
|
||||
@@ -245,7 +250,9 @@ class SensorDeviceClass(StrEnum):
|
||||
Use this device class for sensors measuring stored energy, for example the amount
|
||||
of electric energy currently stored in a battery or the capacity of a battery.
|
||||
|
||||
Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `mWh`, `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `cal`, `kcal`, `Mcal`, `Gcal`
|
||||
Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `mWh`,
|
||||
`Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `cal`, `kcal`,
|
||||
`Mcal`, `Gcal`
|
||||
"""
|
||||
|
||||
FREQUENCY = "frequency"
|
||||
@@ -464,8 +471,10 @@ class SensorDeviceClass(StrEnum):
|
||||
|
||||
Unit of measurement: `VOLUME_*` units
|
||||
- SI / metric: `mL`, `L`, `m³`
|
||||
- USCS / imperial: `ft³`, `CCF`, `MCF`, `fl. oz.`, `gal` (warning: volumes expressed in
|
||||
USCS/imperial units are currently assumed to be US volumes)
|
||||
- USCS / imperial: `ft³`, `CCF`, `MCF`,
|
||||
`fl. oz.`, `gal` (warning: volumes expressed in
|
||||
USCS/imperial units are currently assumed to be
|
||||
US volumes)
|
||||
"""
|
||||
|
||||
VOLUME_STORAGE = "volume_storage"
|
||||
@@ -476,8 +485,10 @@ class SensorDeviceClass(StrEnum):
|
||||
|
||||
Unit of measurement: `VOLUME_*` units
|
||||
- SI / metric: `mL`, `L`, `m³`
|
||||
- USCS / imperial: `ft³`, `CCF`, `MCF`, `fl. oz.`, `gal` (warning: volumes expressed in
|
||||
USCS/imperial units are currently assumed to be US volumes)
|
||||
- USCS / imperial: `ft³`, `CCF`, `MCF`,
|
||||
`fl. oz.`, `gal` (warning: volumes expressed in
|
||||
USCS/imperial units are currently assumed to be
|
||||
US volumes)
|
||||
"""
|
||||
|
||||
VOLUME_FLOW_RATE = "volume_flow_rate"
|
||||
@@ -545,7 +556,10 @@ class SensorStateClass(StrEnum):
|
||||
"""The state represents a measurement in present time."""
|
||||
|
||||
MEASUREMENT_ANGLE = "measurement_angle"
|
||||
"""The state represents a angle measurement in present time. Currently only degrees are supported."""
|
||||
"""The state represents an angle measurement in present time.
|
||||
|
||||
Currently only degrees are supported.
|
||||
"""
|
||||
|
||||
TOTAL = "total"
|
||||
"""The state represents a total amount.
|
||||
|
||||
@@ -92,7 +92,8 @@ WARN_NEGATIVE: HassKey[set[str]] = HassKey(f"{DOMAIN}_warn_total_increasing_nega
|
||||
# Keep track of entities for which a warning about unsupported unit has been logged
|
||||
WARN_UNSUPPORTED_UNIT: HassKey[set[str]] = HassKey(f"{DOMAIN}_warn_unsupported_unit")
|
||||
WARN_UNSTABLE_UNIT: HassKey[set[str]] = HassKey(f"{DOMAIN}_warn_unstable_unit")
|
||||
# Keep track of entities for which a warning about statistics mean algorithm change has been logged
|
||||
# Keep track of entities for which a warning about
|
||||
# statistics mean algorithm change has been logged
|
||||
WARN_STATISTICS_MEAN_CHANGED: HassKey[set[str]] = HassKey(
|
||||
f"{DOMAIN}_warn_statistics_mean_change"
|
||||
)
|
||||
@@ -165,8 +166,8 @@ def _time_weighted_circular_mean(
|
||||
) -> tuple[float, float]:
|
||||
"""Calculate a time weighted circular mean.
|
||||
|
||||
The circular mean is calculated by weighting the states by duration in seconds between
|
||||
state changes.
|
||||
The circular mean is calculated by weighting the states
|
||||
by duration in seconds between state changes.
|
||||
Note: there's no interpolation of values between state changes.
|
||||
"""
|
||||
old_fstate: float | None = None
|
||||
@@ -407,11 +408,12 @@ def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str:
|
||||
def warn_dip(
|
||||
hass: HomeAssistant, entity_id: str, state: State, previous_fstate: float
|
||||
) -> None:
|
||||
"""Log a warning once if a sensor with state class TOTAL_INCREASING has a decreasing value.
|
||||
"""Log a warning once if a sensor with TOTAL_INCREASING has a decreasing value.
|
||||
|
||||
The log will be suppressed until two dips have been seen to prevent warning due to
|
||||
rounding issues with databases storing the state as a single precision float, which
|
||||
was fixed in recorder DB version 20.
|
||||
The log will be suppressed until two dips have been seen
|
||||
to prevent warning due to rounding issues with databases
|
||||
storing the state as a single precision float, which was
|
||||
fixed in recorder DB version 20.
|
||||
"""
|
||||
if SEEN_DIP not in hass.data:
|
||||
hass.data[SEEN_DIP] = set()
|
||||
@@ -443,7 +445,7 @@ def warn_dip(
|
||||
|
||||
|
||||
def warn_negative(hass: HomeAssistant, entity_id: str, state: State) -> None:
|
||||
"""Log a warning once if a sensor with state class TOTAL_INCREASING has a negative value."""
|
||||
"""Log a warning once if a sensor with TOTAL_INCREASING has a negative value."""
|
||||
if WARN_NEGATIVE not in hass.data:
|
||||
hass.data[WARN_NEGATIVE] = set()
|
||||
if entity_id not in hass.data[WARN_NEGATIVE]:
|
||||
@@ -662,7 +664,8 @@ def compile_statistics( # noqa: C901
|
||||
hass.data[WARN_STATISTICS_MEAN_CHANGED].add(entity_id)
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"The statistics mean algorithm for %s have changed from %s to %s."
|
||||
"The statistics mean algorithm for %s have"
|
||||
" changed from %s to %s."
|
||||
" Generation of long term statistics will be suppressed"
|
||||
" unless it changes back or go to %s to delete the old"
|
||||
" statistics"
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
|
||||
class SENZConfigEntryAuth(AbstractSENZAuth):
|
||||
"""Provide nVent RAYCHEM SENZ authentication tied to an OAuth2 based config entry."""
|
||||
"""Provide nVent RAYCHEM SENZ authentication tied to an OAuth2 config entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -89,7 +89,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: SFTPConfigEntry) -> Non
|
||||
pkey.unlink()
|
||||
except OSError as e:
|
||||
LOGGER.warning(
|
||||
"Failed to remove private key %s for %s integration for host %s@%s. %s",
|
||||
"Failed to remove private key %s for %s"
|
||||
" integration for host %s@%s. %s",
|
||||
pkey.name,
|
||||
DOMAIN,
|
||||
entry.data[CONF_USERNAME],
|
||||
@@ -103,7 +104,9 @@ async def async_remove_entry(hass: HomeAssistant, entry: SFTPConfigEntry) -> Non
|
||||
if e.errno == errno.ENOTEMPTY: # Directory not empty
|
||||
if LOGGER.isEnabledFor(logging.DEBUG):
|
||||
leftover_files = []
|
||||
# If we get an exception while gathering leftover files, make sure to log plain message.
|
||||
# If we get an exception while gathering
|
||||
# leftover files, make sure to log plain
|
||||
# message.
|
||||
with contextlib.suppress(OSError):
|
||||
leftover_files = [f.name for f in pkey.parent.iterdir()]
|
||||
|
||||
@@ -117,7 +120,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: SFTPConfigEntry) -> Non
|
||||
)
|
||||
else:
|
||||
LOGGER.warning(
|
||||
"Error occurred while removing directory %s for integration %s: %s at host %s@%s",
|
||||
"Error occurred while removing directory %s"
|
||||
" for integration %s: %s at host %s@%s",
|
||||
str(pkey.parent),
|
||||
DOMAIN,
|
||||
str(e),
|
||||
|
||||
@@ -67,7 +67,8 @@ class SFTPBackupAgent(BackupAgent):
|
||||
) -> AsyncIterator[bytes]:
|
||||
"""Download a backup file from SFTP."""
|
||||
LOGGER.debug(
|
||||
"Establishing SFTP connection to remote host in order to download backup id: %s",
|
||||
"Establishing SFTP connection to remote host"
|
||||
" in order to download backup id: %s",
|
||||
backup_id,
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def get_client_options(cfg: SFTPConfigEntryData) -> SSHClientConnectionOptions:
|
||||
"""Use this function with `hass.async_add_executor_job` to asynchronously get `SSHClientConnectionOptions`."""
|
||||
"""Get `SSHClientConnectionOptions` for use with `hass.async_add_executor_job`."""
|
||||
|
||||
return SSHClientConnectionOptions(
|
||||
known_hosts=None,
|
||||
@@ -177,7 +177,8 @@ class BackupAgentClient:
|
||||
if not await self.sftp.exists(metadata.file_path):
|
||||
await self.sftp.unlink(metadata.metadata_file)
|
||||
raise FileNotFoundError(
|
||||
f"File at provided remote location: {metadata.file_path} does not exist."
|
||||
"File at provided remote location:"
|
||||
f" {metadata.file_path} does not exist."
|
||||
)
|
||||
|
||||
LOGGER.debug("Removing file at path: %s", metadata.file_path)
|
||||
@@ -186,7 +187,7 @@ class BackupAgentClient:
|
||||
await self.sftp.unlink(metadata.metadata_file)
|
||||
|
||||
async def async_list_backups(self) -> list[AgentBackup]:
|
||||
"""Iterate through a list of metadata files and return a list of `AgentBackup` objects."""
|
||||
"""Iterate through metadata files and return a list of `AgentBackup` objects."""
|
||||
|
||||
backups: list[AgentBackup] = []
|
||||
|
||||
@@ -217,7 +218,7 @@ class BackupAgentClient:
|
||||
iterator: AsyncIterator[bytes],
|
||||
backup: AgentBackup,
|
||||
) -> None:
|
||||
"""Accept `iterator` as bytes iterator and write backup archive to SFTP Server."""
|
||||
"""Accept `iterator` as bytes iterator and write backup archive."""
|
||||
|
||||
file_path = (
|
||||
f"{self.cfg.runtime_data.backup_location}/{suggested_filename(backup)}"
|
||||
@@ -244,8 +245,10 @@ class BackupAgentClient:
|
||||
async def iter_file(self, backup_id: str) -> AsyncFileIterator:
|
||||
"""Return Async File Iterator object.
|
||||
|
||||
`SFTPClientFile` object (that would be returned with `sftp.open`) is not an iterator.
|
||||
So we return custom made class - `AsyncFileIterator` that would allow iteration on file object.
|
||||
`SFTPClientFile` object (that would be returned with
|
||||
`sftp.open`) is not an iterator. So we return custom
|
||||
made class - `AsyncFileIterator` that would allow
|
||||
iteration on file object.
|
||||
|
||||
Raises:
|
||||
------
|
||||
@@ -294,7 +297,9 @@ class BackupAgentClient:
|
||||
)
|
||||
except (OSError, PermissionDenied) as e:
|
||||
raise BackupAgentError(
|
||||
"Failure while attempting to establish SSH connection. Please check SSH credentials and if changed, re-install the integration"
|
||||
"Failure while attempting to establish SSH"
|
||||
" connection. Please check SSH credentials"
|
||||
" and if changed, re-install the integration"
|
||||
) from e
|
||||
|
||||
# Configure SFTP Client Connection
|
||||
@@ -303,7 +308,8 @@ class BackupAgentClient:
|
||||
await self.sftp.chdir(self.cfg.runtime_data.backup_location)
|
||||
except (SFTPNoSuchFile, SFTPPermissionDenied) as e:
|
||||
raise BackupAgentError(
|
||||
"Failed to create SFTP client. Re-installing integration might be required"
|
||||
"Failed to create SFTP client."
|
||||
" Re-installing integration might be required"
|
||||
) from e
|
||||
|
||||
return self
|
||||
|
||||
@@ -58,11 +58,11 @@ class SFTPStorageException(Exception):
|
||||
|
||||
|
||||
class SFTPStorageInvalidPrivateKey(SFTPStorageException):
|
||||
"""Exception raised during config flow - when user provided invalid private key file."""
|
||||
"""Exception raised when user provided invalid private key file."""
|
||||
|
||||
|
||||
class SFTPStorageMissingPasswordOrPkey(SFTPStorageException):
|
||||
"""Exception raised during config flow - when user did not provide password or private key file."""
|
||||
"""Exception raised when user did not provide password or private key file."""
|
||||
|
||||
|
||||
class SFTPFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@@ -85,8 +85,10 @@ class SFTPFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
Returns: the possibly updated `user_input`.
|
||||
|
||||
Raises:
|
||||
- SFTPStorageMissingPasswordOrPkey: Neither password nor private key provided
|
||||
- SFTPStorageInvalidPrivateKey: The provided private key has an invalid format
|
||||
- SFTPStorageMissingPasswordOrPkey: Neither password
|
||||
nor private key provided
|
||||
- SFTPStorageInvalidPrivateKey: The provided private
|
||||
key has an invalid format
|
||||
"""
|
||||
|
||||
# If neither password nor private key is provided, error out;
|
||||
@@ -153,9 +155,11 @@ class SFTPFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# - OSError, if host or port are not correct.
|
||||
# - SFTPStorageInvalidPrivateKey, if private key is not valid format.
|
||||
# - asyncssh.misc.PermissionDenied, if credentials are not correct.
|
||||
# - SFTPStorageMissingPasswordOrPkey, if password and private key are not provided.
|
||||
# - SFTPStorageMissingPasswordOrPkey, if password
|
||||
# and private key are not provided.
|
||||
# - asyncssh.sftp.SFTPNoSuchFile, if directory does not exist.
|
||||
# - asyncssh.sftp.SFTPPermissionDenied, if we don't have access to said directory
|
||||
# - asyncssh.sftp.SFTPPermissionDenied,
|
||||
# if we don't have access to said directory
|
||||
async with (
|
||||
connect(
|
||||
host=user_config.host,
|
||||
|
||||
@@ -71,7 +71,9 @@ async def _validate_input(
|
||||
LOGGER.exception("Unexpected exception")
|
||||
LOGGER.error(error)
|
||||
raise UnknownAuth(
|
||||
"An unknown error occurred. Check your region settings and open an issue on Github if the issue persists."
|
||||
"An unknown error occurred. Check your region"
|
||||
" settings and open an issue on GitHub"
|
||||
" if the issue persists."
|
||||
) from error
|
||||
|
||||
# Return info that you want to store in the config entry.
|
||||
|
||||
@@ -137,9 +137,10 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum
|
||||
def activity(self) -> VacuumActivity | None:
|
||||
"""Get the current vacuum state.
|
||||
|
||||
NB: Currently, we do not return an error state because they can be very, very stale.
|
||||
In the app, these are (usually) handled by showing the robot as stopped and sending the
|
||||
user a notification.
|
||||
NB: Currently, we do not return an error state
|
||||
because they can be very, very stale. In the app,
|
||||
these are (usually) handled by showing the robot as
|
||||
stopped and sending the user a notification.
|
||||
"""
|
||||
if self.sharkiq.get_property_value(Properties.CHARGING_STATUS):
|
||||
return VacuumActivity.DOCKED
|
||||
|
||||
@@ -236,7 +236,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
and isinstance(model_id, int)
|
||||
and (model_name := get_name_from_model_id(model_id))
|
||||
):
|
||||
# Remove spaces from model name (e.g., "Shelly 1 Mini Gen4" -> "Shelly1MiniGen4")
|
||||
# Remove spaces from model name
|
||||
# (e.g., "Shelly 1 Mini Gen4" -> "Shelly1MiniGen4")
|
||||
return f"{model_name.replace(' ', '')}-{mac}"
|
||||
return f"Shelly-{mac}"
|
||||
|
||||
@@ -407,11 +408,13 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def _async_connect_and_get_info(
|
||||
self, host: str, port: int
|
||||
) -> ConfigFlowResult | None:
|
||||
"""Connect to device, validate, and create entry or return None to continue flow.
|
||||
"""Connect to device, validate, and create entry or return None.
|
||||
|
||||
This helper consolidates the common logic between Zeroconf device selection
|
||||
and manual entry flows. Returns a ConfigFlowResult if the flow should end
|
||||
(create_entry or abort), or None if the flow should continue (e.g., to credentials).
|
||||
This helper consolidates the common logic between
|
||||
Zeroconf device selection and manual entry flows.
|
||||
Returns a ConfigFlowResult if the flow should end
|
||||
(create_entry or abort), or None if the flow should
|
||||
continue (e.g., to credentials).
|
||||
|
||||
Sets self.info, self.host, and self.port on success.
|
||||
"""
|
||||
@@ -685,7 +688,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self._async_discovered_mac(mac, host)
|
||||
|
||||
async def _async_discovered_mac(self, mac: str, host: str) -> None:
|
||||
"""Abort and reconnect soon if the device with the mac address is already configured."""
|
||||
"""Abort and reconnect soon if the device with the mac is already configured."""
|
||||
if (
|
||||
current_entry := await self.async_set_unique_id(mac)
|
||||
) and current_entry.data.get(CONF_HOST) == host:
|
||||
@@ -914,7 +917,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult | None:
|
||||
"""Provision WiFi credentials via BLE and wait for zeroconf discovery.
|
||||
|
||||
Returns the flow result to be stored in self._provision_result, or None if failed.
|
||||
Returns the flow result to be stored in
|
||||
self._provision_result, or None if failed.
|
||||
"""
|
||||
# Provision WiFi via BLE using persistent connection
|
||||
try:
|
||||
@@ -974,7 +978,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
state.port = DEFAULT_HTTP_PORT
|
||||
else:
|
||||
LOGGER.debug("BLE fallback also failed - provisioning unsuccessful")
|
||||
# Store failure info and return None - provision_done will handle redirect
|
||||
# Store failure info and return None
|
||||
# provision_done will handle redirect
|
||||
return None
|
||||
else:
|
||||
state.host, state.port = result
|
||||
|
||||
@@ -122,8 +122,9 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
|
||||
self.suggested_area: str | None = None
|
||||
device_name = device.name if device.initialized else entry.title
|
||||
interval_td = timedelta(seconds=update_interval)
|
||||
# The device has come online at least once. In the case of a sleeping RPC
|
||||
# device, this means that the device has connected to the WS server at least once.
|
||||
# The device has come online at least once. In the case
|
||||
# of a sleeping RPC device, this means that the device
|
||||
# has connected to the WS server at least once.
|
||||
self._came_online_once = False
|
||||
super().__init__(
|
||||
hass,
|
||||
|
||||
@@ -163,7 +163,8 @@ def _async_setup_rpc_entry(
|
||||
ShellyRpcScriptEvent(coordinator, script, SCRIPT_EVENT, event_types)
|
||||
)
|
||||
|
||||
# If a script is removed, from the device configuration, we need to remove orphaned entities
|
||||
# If a script is removed, from the device configuration,
|
||||
# we need to remove orphaned entities
|
||||
async_remove_orphaned_entities(
|
||||
hass,
|
||||
config_entry.entry_id,
|
||||
|
||||
@@ -41,7 +41,10 @@ def async_describe_events(
|
||||
rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id)
|
||||
if rpc_coordinator and rpc_coordinator.device.initialized:
|
||||
key = f"input:{channel - 1}"
|
||||
input_name = f"{rpc_coordinator.device.name} {get_rpc_channel_name(rpc_coordinator.device, key)}"
|
||||
input_name = (
|
||||
f"{rpc_coordinator.device.name}"
|
||||
f" {get_rpc_channel_name(rpc_coordinator.device, key)}"
|
||||
)
|
||||
|
||||
elif click_type in BLOCK_INPUTS_EVENTS_TYPES:
|
||||
block_coordinator = get_block_coordinator_by_device_id(hass, device_id)
|
||||
|
||||
@@ -83,6 +83,8 @@ class ListTopItemsIntent(intent.IntentHandler):
|
||||
else:
|
||||
items_list = ", ".join(str(itm["name"]) for itm in reversed(items))
|
||||
response.async_set_speech(
|
||||
f"These are the top {min(len(items), 5)} items on your shopping list: {items_list}"
|
||||
"These are the top"
|
||||
f" {min(len(items), 5)} items on your"
|
||||
f" shopping list: {items_list}"
|
||||
)
|
||||
return response
|
||||
|
||||
@@ -132,7 +132,7 @@ class SIAConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_handle_data_and_route(
|
||||
self, user_input: dict[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the user_input, check if configured and route to the right next step or create entry."""
|
||||
"""Handle user_input, check if configured and route to the right next step."""
|
||||
self._update_data(user_input)
|
||||
|
||||
self._async_abort_entries_match({CONF_PORT: self._data[CONF_PORT]})
|
||||
@@ -148,7 +148,8 @@ class SIAConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
def _update_data(self, user_input: dict[str, Any]) -> None:
|
||||
"""Parse the user_input and store in data and options attributes.
|
||||
|
||||
If there is a port in the input or no data, assume it is fully new and overwrite.
|
||||
If there is a port in the input or no data, assume
|
||||
it is fully new and overwrite.
|
||||
Add the default options and overwrite the zones in options.
|
||||
"""
|
||||
if not self._data or user_input.get(CONF_PORT):
|
||||
|
||||
@@ -116,7 +116,7 @@ class SIABaseEntity(RestoreEntity):
|
||||
|
||||
@callback
|
||||
def async_handle_event(self, sia_event: SIAEvent) -> None:
|
||||
"""Listen to dispatcher events for this port and account and update state and attributes.
|
||||
"""Listen to dispatcher events for this port and account, update state.
|
||||
|
||||
If the event is for either the zone or the 0 zone (hub zone),
|
||||
then handle it further.
|
||||
|
||||
@@ -51,7 +51,7 @@ class SIAHub:
|
||||
|
||||
@callback
|
||||
def async_setup_hub(self) -> None:
|
||||
"""Add a device to the device_registry, register shutdown listener, load reactions."""
|
||||
"""Add a device to the device_registry, register shutdown listener."""
|
||||
self.update_accounts()
|
||||
device_registry = dr.async_get(self._hass)
|
||||
for acc in self._accounts:
|
||||
@@ -74,10 +74,14 @@ class SIAHub:
|
||||
await self.sia_client.async_stop()
|
||||
|
||||
async def async_create_and_fire_event(self, event: SIAEvent) -> None:
|
||||
"""Create a event on HA dispatcher and then on HA's bus, with the data from the SIAEvent.
|
||||
|
||||
The created event is handled by default for only a small subset for each platform (there are about 320 SIA Codes defined, only 22 of those are used in the alarm_control_panel), a user can choose to build other automation or even entities on the same event for SIA codes not handled by the built-in platforms.
|
||||
"""Create an event on HA dispatcher and then on HA's bus.
|
||||
|
||||
The created event is handled by default for only a
|
||||
small subset for each platform (there are about 320
|
||||
SIA Codes defined, only 22 of those are used in the
|
||||
alarm_control_panel), a user can choose to build other
|
||||
automation or even entities on the same event for SIA
|
||||
codes not handled by the built-in platforms.
|
||||
"""
|
||||
_LOGGER.debug(
|
||||
"Adding event to dispatch and bus for code %s for port %s and account %s",
|
||||
@@ -109,7 +113,8 @@ class SIAHub:
|
||||
if self.sia_client is not None:
|
||||
self.sia_client.accounts = self.sia_accounts
|
||||
return
|
||||
# the new client class method creates a subclass based on protocol, hence the type ignore
|
||||
# the new client class method creates a subclass
|
||||
# based on protocol, hence the type ignore
|
||||
self.sia_client = SIAClient(
|
||||
host="",
|
||||
port=self._port,
|
||||
@@ -119,7 +124,7 @@ class SIAHub:
|
||||
)
|
||||
|
||||
def _load_options(self) -> None:
|
||||
"""Store attributes to avoid property call overhead since they are called frequently."""
|
||||
"""Store attributes to avoid property call overhead."""
|
||||
options = dict(self._entry.options)
|
||||
for acc in self._accounts:
|
||||
acc_id = acc[CONF_ACCOUNT]
|
||||
@@ -135,9 +140,10 @@ class SIAHub:
|
||||
) -> None:
|
||||
"""Handle signals of config entry being updated.
|
||||
|
||||
First, update the accounts, this will reflect any changes with ignore_timestamps.
|
||||
Second, unload underlying platforms, and then setup platforms, this reflects any changes in number of zones.
|
||||
|
||||
First, update the accounts, this will reflect any
|
||||
changes with ignore_timestamps. Second, unload
|
||||
underlying platforms, and then setup platforms, this
|
||||
reflects any changes in number of zones.
|
||||
"""
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
return
|
||||
|
||||
@@ -97,7 +97,7 @@ class SignalNotificationService(BaseNotificationService):
|
||||
self._signal_cli_rest_api = signal_cli_rest_api
|
||||
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to one or more recipients. Additionally a file can be attached."""
|
||||
"""Send a message to one or more recipients."""
|
||||
|
||||
_LOGGER.debug("Sending signal message")
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ class SimpleFinDataUpdateCoordinator(DataUpdateCoordinator[FinancialData]):
|
||||
|
||||
except SimpleFinPaymentRequiredError as err:
|
||||
LOGGER.warning(
|
||||
"There is a billing issue with your SimpleFin account, contact Simplefin to address this issue"
|
||||
"There is a billing issue with your SimpleFin"
|
||||
" account, contact SimpleFin to address"
|
||||
" this issue"
|
||||
)
|
||||
raise UpdateFailed from err
|
||||
|
||||
@@ -350,8 +350,11 @@ class SlackNotificationService(BaseNotificationService):
|
||||
channel_name = channel_name.lstrip("#")
|
||||
|
||||
# Get channel list
|
||||
# Multiple types is not working. Tested here: https://api.slack.com/methods/conversations.list/test
|
||||
# response = await self._client.conversations_list(types="public_channel,private_channel")
|
||||
# Multiple types is not working. Tested here:
|
||||
# https://api.slack.com/methods/conversations.list/test
|
||||
# response = await self._client.conversations_list(
|
||||
# types="public_channel,private_channel"
|
||||
# )
|
||||
#
|
||||
# Workaround for the types parameter not working
|
||||
channels = []
|
||||
|
||||
@@ -24,12 +24,14 @@ async def upload_file_to_slack(
|
||||
Args:
|
||||
client (AsyncWebClient): The Slack WebClient instance.
|
||||
channel_ids (list[str | None]): List of channel IDs to upload the file to.
|
||||
file_content (Union[bytes, str, None]): Content of the file (local or remote). If None, file_path is used.
|
||||
file_content (Union[bytes, str, None]): Content of the
|
||||
file (local or remote). If None, file_path is used.
|
||||
filename (str): The file's name.
|
||||
title (str | None): Title of the file in Slack.
|
||||
message (str): Initial comment to accompany the file.
|
||||
thread_ts (str | None): Thread timestamp for threading messages.
|
||||
file_path (str | None): Path to the local file to be read if file_content is None.
|
||||
file_path (str | None): Path to the local file to be
|
||||
read if file_content is None.
|
||||
|
||||
Raises:
|
||||
SlackApiError: If the Slack API call fails.
|
||||
|
||||
@@ -59,7 +59,9 @@ def _get_actuator_name(bed: SleepIQBed, actuator: SleepIQActuator) -> str:
|
||||
if actuator.side:
|
||||
return (
|
||||
"SleepNumber"
|
||||
f" {bed.name} {actuator.side_full} {actuator.actuator_full} {ENTITY_TYPES[ACTUATOR]}"
|
||||
f" {bed.name} {actuator.side_full}"
|
||||
f" {actuator.actuator_full}"
|
||||
f" {ENTITY_TYPES[ACTUATOR]}"
|
||||
)
|
||||
|
||||
return f"SleepNumber {bed.name} {actuator.actuator_full} {ENTITY_TYPES[ACTUATOR]}"
|
||||
|
||||
@@ -100,7 +100,8 @@ class SlideCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
if not self.config_entry.options.get(CONF_INVERT_POSITION, False):
|
||||
# For slide 0->open, 1->closed; for HA 0->closed, 1->open
|
||||
# Value has therefore to be inverted, unless CONF_INVERT_POSITION is true
|
||||
# Value has therefore to be inverted,
|
||||
# unless CONF_INVERT_POSITION is true
|
||||
data["pos"] = 1 - data["pos"]
|
||||
|
||||
if oldpos is None or oldpos == data["pos"]:
|
||||
|
||||
@@ -255,7 +255,8 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
entry, data_updates={CONF_MAC: self._data[CONF_MAC]}
|
||||
)
|
||||
|
||||
# Finally, check if the hostname (which represents the SMA serial number) is unique
|
||||
# Finally, check if the hostname
|
||||
# (which represents the SMA serial number) is unique
|
||||
serial_number = discovery_info.hostname.lower()
|
||||
# Example hostname: sma12345678-01
|
||||
# Remove 'sma' prefix and strip everything after the dash (including the dash)
|
||||
|
||||
@@ -135,7 +135,8 @@ class SmappeeFlowHandler(
|
||||
errors={},
|
||||
)
|
||||
|
||||
# Environment chosen, request additional host information for LOCAL or OAuth2 flow for CLOUD
|
||||
# Environment chosen, request additional host information
|
||||
# for LOCAL or OAuth2 flow for CLOUD
|
||||
# Ask for host detail
|
||||
if user_input["environment"] == ENV_LOCAL:
|
||||
return await self.async_step_local()
|
||||
@@ -160,7 +161,8 @@ class SmappeeFlowHandler(
|
||||
ip_address = user_input["host"]
|
||||
serial_number = None
|
||||
|
||||
# Attempt 1: try to use the local api (older generation) to resolve host to serialnumber
|
||||
# Attempt 1: try to use the local api (older generation)
|
||||
# to resolve host to serialnumber
|
||||
smappee_api = api.api.SmappeeLocalApi(ip=ip_address)
|
||||
logon = await self.hass.async_add_executor_job(smappee_api.logon)
|
||||
if logon is not None:
|
||||
@@ -171,7 +173,8 @@ class SmappeeFlowHandler(
|
||||
if config_item["key"] == "mdnsHostName":
|
||||
serial_number = config_item["value"]
|
||||
else:
|
||||
# Attempt 2: try to use the local mqtt broker (newer generation) to resolve host to serialnumber
|
||||
# Attempt 2: try to use the local mqtt broker
|
||||
# (newer generation) to resolve host to serialnumber
|
||||
smappee_mqtt = mqtt.SmappeeLocalMqtt()
|
||||
connect = await self.hass.async_add_executor_job(smappee_mqtt.start_attempt)
|
||||
if not connect:
|
||||
|
||||
@@ -156,7 +156,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
|
||||
|
||||
def _handle_max_connections() -> None:
|
||||
_LOGGER.debug(
|
||||
"We hit the limit of max connections or we could not remove the old one, so retrying"
|
||||
"We hit the limit of max connections or we could"
|
||||
" not remove the old one, so retrying"
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
@@ -335,7 +336,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Handle config entry migration."""
|
||||
|
||||
if entry.version < 3:
|
||||
# We keep the old data around, so we can use that to clean up the webhook in the future
|
||||
# We keep the old data around, so we can use that
|
||||
# to clean up the webhook in the future
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, version=3, data={OLD_DATA: dict(entry.data)}
|
||||
)
|
||||
@@ -377,7 +379,12 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"energySaved_meter",
|
||||
}:
|
||||
return {
|
||||
"new_unique_id": f"{device_id}_{MAIN}_{Capability.POWER_CONSUMPTION_REPORT}_{Attribute.POWER_CONSUMPTION}_{attribute}",
|
||||
"new_unique_id": (
|
||||
f"{device_id}_{MAIN}"
|
||||
f"_{Capability.POWER_CONSUMPTION_REPORT}"
|
||||
f"_{Attribute.POWER_CONSUMPTION}"
|
||||
f"_{attribute}"
|
||||
),
|
||||
}
|
||||
if attribute in {
|
||||
"X Coordinate",
|
||||
@@ -390,7 +397,12 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"Z Coordinate": "z_coordinate",
|
||||
}[attribute]
|
||||
return {
|
||||
"new_unique_id": f"{device_id}_{MAIN}_{Capability.THREE_AXIS}_{Attribute.THREE_AXIS}_{new_attribute}",
|
||||
"new_unique_id": (
|
||||
f"{device_id}_{MAIN}"
|
||||
f"_{Capability.THREE_AXIS}"
|
||||
f"_{Attribute.THREE_AXIS}"
|
||||
f"_{new_attribute}"
|
||||
),
|
||||
}
|
||||
if attribute in {
|
||||
Attribute.MACHINE_STATE,
|
||||
@@ -402,16 +414,27 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if capability is None:
|
||||
return None
|
||||
return {
|
||||
"new_unique_id": f"{device_id}_{MAIN}_{capability}_{attribute}_{attribute}",
|
||||
"new_unique_id": (
|
||||
f"{device_id}_{MAIN}"
|
||||
f"_{capability}"
|
||||
f"_{attribute}_{attribute}"
|
||||
),
|
||||
}
|
||||
return None
|
||||
return {
|
||||
"new_unique_id": f"{device_id}_{MAIN}_{capability}_{attribute}_{attribute}",
|
||||
"new_unique_id": (
|
||||
f"{device_id}_{MAIN}_{capability}_{attribute}_{attribute}"
|
||||
),
|
||||
}
|
||||
|
||||
if entity_entry.domain == "switch":
|
||||
return {
|
||||
"new_unique_id": f"{entity_entry.unique_id}_{MAIN}_{Capability.SWITCH}_{Attribute.SWITCH}_{Attribute.SWITCH}",
|
||||
"new_unique_id": (
|
||||
f"{entity_entry.unique_id}_{MAIN}"
|
||||
f"_{Capability.SWITCH}"
|
||||
f"_{Attribute.SWITCH}"
|
||||
f"_{Attribute.SWITCH}"
|
||||
),
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
@@ -328,7 +328,10 @@ class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity):
|
||||
self._attribute = attribute
|
||||
self.capability = capability
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{device.device.device_id}_{component}_{capability}_{attribute}_{attribute}"
|
||||
self._attr_unique_id = (
|
||||
f"{device.device.device_id}_{component}"
|
||||
f"_{capability}_{attribute}_{attribute}"
|
||||
)
|
||||
if (
|
||||
entity_description.category_device_class
|
||||
and (category := get_main_component_category(device))
|
||||
|
||||
@@ -167,7 +167,11 @@ class SmartThingsButtonEntity(SmartThingsEntity, ButtonEntity):
|
||||
super().__init__(client, device, capabilities)
|
||||
self.entity_description = entity_description
|
||||
self.button_capability = capability
|
||||
self._attr_unique_id = f"{device.device.device_id}_{component}_{entity_description.key}_{entity_description.command}"
|
||||
self._attr_unique_id = (
|
||||
f"{device.device.device_id}_{component}"
|
||||
f"_{entity_description.key}"
|
||||
f"_{entity_description.command}"
|
||||
)
|
||||
if entity_description.command_identifier is not None:
|
||||
self._attr_unique_id += f"_{entity_description.command_identifier}"
|
||||
|
||||
|
||||
@@ -434,8 +434,9 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
tasks.append(self.async_turn_on())
|
||||
|
||||
mode = STATE_TO_AC_MODE[hvac_mode]
|
||||
# If new hvac_mode is HVAC_MODE_FAN_ONLY and AirConditioner support "wind" or "fan" mode the AirConditioner
|
||||
# new mode has to be "wind" or "fan"
|
||||
# If new hvac_mode is HVAC_MODE_FAN_ONLY and
|
||||
# AirConditioner supports "wind" or "fan" mode,
|
||||
# the AirConditioner new mode has to be "wind" or "fan"
|
||||
if hvac_mode == HVACMode.FAN_ONLY:
|
||||
for fan_mode in (WIND, FAN):
|
||||
if fan_mode in self.get_attribute_value(
|
||||
|
||||
@@ -69,7 +69,9 @@ SENSOR_ATTRIBUTES_TO_CAPABILITIES: dict[str, str] = {
|
||||
Attribute.DUST_LEVEL: Capability.DUST_SENSOR,
|
||||
Attribute.FINE_DUST_LEVEL: Capability.DUST_SENSOR,
|
||||
Attribute.ENERGY: Capability.ENERGY_METER,
|
||||
Attribute.EQUIVALENT_CARBON_DIOXIDE_MEASUREMENT: Capability.EQUIVALENT_CARBON_DIOXIDE_MEASUREMENT,
|
||||
Attribute.EQUIVALENT_CARBON_DIOXIDE_MEASUREMENT: (
|
||||
Capability.EQUIVALENT_CARBON_DIOXIDE_MEASUREMENT
|
||||
),
|
||||
Attribute.FORMALDEHYDE_LEVEL: Capability.FORMALDEHYDE_MEASUREMENT,
|
||||
Attribute.GAS_METER: Capability.GAS_METER,
|
||||
Attribute.GAS_METER_CALORIFIC: Capability.GAS_METER,
|
||||
|
||||
@@ -60,7 +60,12 @@ class SmartThingsWasherRinseCyclesNumberEntity(SmartThingsEntity, NumberEntity):
|
||||
def __init__(self, client: SmartThings, device: FullDevice) -> None:
|
||||
"""Initialize the instance."""
|
||||
super().__init__(client, device, {Capability.CUSTOM_WASHER_RINSE_CYCLES})
|
||||
self._attr_unique_id = f"{device.device.device_id}_{MAIN}_{Capability.CUSTOM_WASHER_RINSE_CYCLES}_{Attribute.WASHER_RINSE_CYCLES}_{Attribute.WASHER_RINSE_CYCLES}"
|
||||
self._attr_unique_id = (
|
||||
f"{device.device.device_id}_{MAIN}"
|
||||
f"_{Capability.CUSTOM_WASHER_RINSE_CYCLES}"
|
||||
f"_{Attribute.WASHER_RINSE_CYCLES}"
|
||||
f"_{Attribute.WASHER_RINSE_CYCLES}"
|
||||
)
|
||||
|
||||
@property
|
||||
def options(self) -> list[int]:
|
||||
@@ -112,7 +117,12 @@ class SmartThingsHoodNumberEntity(SmartThingsEntity, NumberEntity):
|
||||
super().__init__(
|
||||
client, device, {Capability.SAMSUNG_CE_HOOD_FAN_SPEED}, component="hood"
|
||||
)
|
||||
self._attr_unique_id = f"{device.device.device_id}_hood_{Capability.SAMSUNG_CE_HOOD_FAN_SPEED}_{Attribute.HOOD_FAN_SPEED}_{Attribute.HOOD_FAN_SPEED}"
|
||||
self._attr_unique_id = (
|
||||
f"{device.device.device_id}_hood"
|
||||
f"_{Capability.SAMSUNG_CE_HOOD_FAN_SPEED}"
|
||||
f"_{Attribute.HOOD_FAN_SPEED}"
|
||||
f"_{Attribute.HOOD_FAN_SPEED}"
|
||||
)
|
||||
|
||||
@property
|
||||
def options(self) -> list[int]:
|
||||
@@ -169,7 +179,12 @@ class SmartThingsRefrigeratorTemperatureNumberEntity(SmartThingsEntity, NumberEn
|
||||
{Capability.THERMOSTAT_COOLING_SETPOINT},
|
||||
component=component,
|
||||
)
|
||||
self._attr_unique_id = f"{device.device.device_id}_{component}_{Capability.THERMOSTAT_COOLING_SETPOINT}_{Attribute.COOLING_SETPOINT}_{Attribute.COOLING_SETPOINT}"
|
||||
self._attr_unique_id = (
|
||||
f"{device.device.device_id}_{component}"
|
||||
f"_{Capability.THERMOSTAT_COOLING_SETPOINT}"
|
||||
f"_{Attribute.COOLING_SETPOINT}"
|
||||
f"_{Attribute.COOLING_SETPOINT}"
|
||||
)
|
||||
unit = self._internal_state[Capability.THERMOSTAT_COOLING_SETPOINT][
|
||||
Attribute.COOLING_SETPOINT
|
||||
].unit
|
||||
|
||||
@@ -165,13 +165,15 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
|
||||
command=Command.SET_AMOUNT,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
Capability.SAMSUNG_CE_FLEXIBLE_AUTO_DISPENSE_DETERGENT: SmartThingsSelectDescription(
|
||||
key=Capability.SAMSUNG_CE_FLEXIBLE_AUTO_DISPENSE_DETERGENT,
|
||||
translation_key="flexible_detergent_amount",
|
||||
options_attribute=Attribute.SUPPORTED_AMOUNT,
|
||||
status_attribute=Attribute.AMOUNT,
|
||||
command=Command.SET_AMOUNT,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
Capability.SAMSUNG_CE_FLEXIBLE_AUTO_DISPENSE_DETERGENT: (
|
||||
SmartThingsSelectDescription(
|
||||
key=Capability.SAMSUNG_CE_FLEXIBLE_AUTO_DISPENSE_DETERGENT,
|
||||
translation_key="flexible_detergent_amount",
|
||||
options_attribute=Attribute.SUPPORTED_AMOUNT,
|
||||
status_attribute=Attribute.AMOUNT,
|
||||
command=Command.SET_AMOUNT,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
),
|
||||
Capability.SAMSUNG_CE_LAMP: SmartThingsSelectDescription(
|
||||
key=Capability.SAMSUNG_CE_LAMP,
|
||||
@@ -361,7 +363,12 @@ class SmartThingsSelectEntity(SmartThingsEntity, SelectEntity):
|
||||
capabilities.update(extra_capabilities)
|
||||
super().__init__(client, device, capabilities, component=component)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{device.device.device_id}_{component}_{entity_description.key}_{entity_description.status_attribute}_{entity_description.status_attribute}"
|
||||
self._attr_unique_id = (
|
||||
f"{device.device.device_id}_{component}"
|
||||
f"_{entity_description.key}"
|
||||
f"_{entity_description.status_attribute}"
|
||||
f"_{entity_description.status_attribute}"
|
||||
)
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
|
||||
@@ -1319,7 +1319,9 @@ async def async_setup_entry(
|
||||
capability in device.status[MAIN]
|
||||
for capability in capability_list
|
||||
)
|
||||
for capability_list in description.capability_ignore_list
|
||||
for capability_list in (
|
||||
description.capability_ignore_list
|
||||
)
|
||||
)
|
||||
)
|
||||
and (
|
||||
@@ -1401,7 +1403,11 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
|
||||
if entity_description.use_temperature_unit:
|
||||
capabilities_to_subscribe.add(Capability.TEMPERATURE_MEASUREMENT)
|
||||
super().__init__(client, device, capabilities_to_subscribe, component=component)
|
||||
self._attr_unique_id = f"{device.device.device_id}_{component}_{capability}_{attribute}_{entity_description.key}"
|
||||
self._attr_unique_id = (
|
||||
f"{device.device.device_id}_{component}"
|
||||
f"_{capability}_{attribute}"
|
||||
f"_{entity_description.key}"
|
||||
)
|
||||
self._attribute = attribute
|
||||
self.capability = capability
|
||||
self.entity_description = entity_description
|
||||
|
||||
@@ -85,12 +85,14 @@ CAPABILITY_TO_COMMAND_SWITCHES: dict[
|
||||
command=Command.SET_SPI_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
Capability.SAMSUNG_CE_AIR_CONDITIONER_LIGHTING: SmartThingsCommandSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_AIR_CONDITIONER_LIGHTING,
|
||||
translation_key="display_lighting",
|
||||
status_attribute=Attribute.LIGHTING,
|
||||
command=Command.SET_LIGHTING_LEVEL,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
Capability.SAMSUNG_CE_AIR_CONDITIONER_LIGHTING: (
|
||||
SmartThingsCommandSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_AIR_CONDITIONER_LIGHTING,
|
||||
translation_key="display_lighting",
|
||||
status_attribute=Attribute.LIGHTING,
|
||||
command=Command.SET_LIGHTING_LEVEL,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
),
|
||||
Capability.CUSTOM_DRYER_WRINKLE_PREVENT: SmartThingsCommandSwitchEntityDescription(
|
||||
key=Capability.CUSTOM_DRYER_WRINKLE_PREVENT,
|
||||
@@ -99,21 +101,25 @@ CAPABILITY_TO_COMMAND_SWITCHES: dict[
|
||||
command=Command.SET_DRYER_WRINKLE_PREVENT,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
Capability.SAMSUNG_CE_STEAM_CLOSET_AUTO_CYCLE_LINK: SmartThingsCommandSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_STEAM_CLOSET_AUTO_CYCLE_LINK,
|
||||
translation_key="auto_cycle_link",
|
||||
status_attribute=Attribute.STEAM_CLOSET_AUTO_CYCLE_LINK,
|
||||
command=Command.SET_STEAM_CLOSET_AUTO_CYCLE_LINK,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
Capability.SAMSUNG_CE_STEAM_CLOSET_AUTO_CYCLE_LINK: (
|
||||
SmartThingsCommandSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_STEAM_CLOSET_AUTO_CYCLE_LINK,
|
||||
translation_key="auto_cycle_link",
|
||||
status_attribute=Attribute.STEAM_CLOSET_AUTO_CYCLE_LINK,
|
||||
command=Command.SET_STEAM_CLOSET_AUTO_CYCLE_LINK,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
),
|
||||
Capability.SAMSUNG_CE_MICROFIBER_FILTER_SETTINGS: SmartThingsCommandSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_MICROFIBER_FILTER_SETTINGS,
|
||||
translation_key="bypass_mode",
|
||||
status_attribute=Attribute.BYPASS_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
on_key="enabled",
|
||||
off_key="disabled",
|
||||
command=Command.SET_BYPASS_MODE,
|
||||
Capability.SAMSUNG_CE_MICROFIBER_FILTER_SETTINGS: (
|
||||
SmartThingsCommandSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_MICROFIBER_FILTER_SETTINGS,
|
||||
translation_key="bypass_mode",
|
||||
status_attribute=Attribute.BYPASS_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
on_key="enabled",
|
||||
off_key="disabled",
|
||||
command=Command.SET_BYPASS_MODE,
|
||||
)
|
||||
),
|
||||
}
|
||||
CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescription] = {
|
||||
@@ -164,17 +170,21 @@ CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescriptio
|
||||
off_command=Command.DEACTIVATE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
Capability.SAMSUNG_CE_STEAM_CLOSET_SANITIZE_MODE: SmartThingsSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_STEAM_CLOSET_SANITIZE_MODE,
|
||||
translation_key="sanitize",
|
||||
status_attribute=Attribute.STATUS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
Capability.SAMSUNG_CE_STEAM_CLOSET_SANITIZE_MODE: (
|
||||
SmartThingsSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_STEAM_CLOSET_SANITIZE_MODE,
|
||||
translation_key="sanitize",
|
||||
status_attribute=Attribute.STATUS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
),
|
||||
Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE: SmartThingsSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE,
|
||||
translation_key="keep_fresh_mode",
|
||||
status_attribute=Attribute.STATUS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE: (
|
||||
SmartThingsSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE,
|
||||
translation_key="keep_fresh_mode",
|
||||
status_attribute=Attribute.STATUS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
),
|
||||
Capability.CUSTOM_DO_NOT_DISTURB_MODE: SmartThingsSwitchEntityDescription(
|
||||
key=Capability.CUSTOM_DO_NOT_DISTURB_MODE,
|
||||
@@ -193,13 +203,15 @@ CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescriptio
|
||||
on_command=Command.ENABLE_SOUND_DETECTION,
|
||||
off_command=Command.DISABLE_SOUND_DETECTION,
|
||||
),
|
||||
Capability.SAMSUNG_CE_STICK_CLEANER_DUSTBIN_STATUS: SmartThingsSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_STICK_CLEANER_DUSTBIN_STATUS,
|
||||
translation_key="empty_dustbin",
|
||||
status_attribute=Attribute.OPERATING_STATE,
|
||||
on_key="emptying",
|
||||
on_command=Command.START_EMPTYING,
|
||||
off_command=Command.STOP_EMPTYING,
|
||||
Capability.SAMSUNG_CE_STICK_CLEANER_DUSTBIN_STATUS: (
|
||||
SmartThingsSwitchEntityDescription(
|
||||
key=Capability.SAMSUNG_CE_STICK_CLEANER_DUSTBIN_STATUS,
|
||||
translation_key="empty_dustbin",
|
||||
status_attribute=Attribute.OPERATING_STATE,
|
||||
on_key="emptying",
|
||||
on_command=Command.START_EMPTYING,
|
||||
off_command=Command.STOP_EMPTYING,
|
||||
)
|
||||
),
|
||||
}
|
||||
DISHWASHER_WASHING_OPTIONS_TO_SWITCHES: dict[
|
||||
@@ -261,12 +273,14 @@ DISHWASHER_WASHING_OPTIONS_TO_SWITCHES: dict[
|
||||
command=Command.SET_SANITIZE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
Attribute.SANITIZING_WASH: SmartThingsDishwasherWashingOptionSwitchEntityDescription(
|
||||
key=Attribute.SANITIZING_WASH,
|
||||
translation_key="sanitizing_wash",
|
||||
status_attribute=Attribute.SANITIZING_WASH,
|
||||
command=Command.SET_SANITIZING_WASH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
Attribute.SANITIZING_WASH: (
|
||||
SmartThingsDishwasherWashingOptionSwitchEntityDescription(
|
||||
key=Attribute.SANITIZING_WASH,
|
||||
translation_key="sanitizing_wash",
|
||||
status_attribute=Attribute.SANITIZING_WASH,
|
||||
command=Command.SET_SANITIZING_WASH,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
),
|
||||
Attribute.SPEED_BOOSTER: SmartThingsDishwasherWashingOptionSwitchEntityDescription(
|
||||
key=Attribute.SPEED_BOOSTER,
|
||||
@@ -427,7 +441,12 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchEntity):
|
||||
)
|
||||
self.entity_description = entity_description
|
||||
self.switch_capability = capability
|
||||
self._attr_unique_id = f"{device.device.device_id}_{component}_{capability}_{entity_description.status_attribute}_{entity_description.status_attribute}"
|
||||
self._attr_unique_id = (
|
||||
f"{device.device.device_id}_{component}"
|
||||
f"_{capability}"
|
||||
f"_{entity_description.status_attribute}"
|
||||
f"_{entity_description.status_attribute}"
|
||||
)
|
||||
if (
|
||||
translation_keys := entity_description.component_translation_key
|
||||
) is not None and (
|
||||
|
||||
@@ -67,7 +67,12 @@ class SmartThingsDnDTime(SmartThingsEntity, TimeEntity):
|
||||
"""Initialize the time entity."""
|
||||
super().__init__(client, device, {Capability.CUSTOM_DO_NOT_DISTURB_MODE})
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{device.device.device_id}_{MAIN}_{Capability.CUSTOM_DO_NOT_DISTURB_MODE}_{entity_description.attribute}_{entity_description.attribute}"
|
||||
self._attr_unique_id = (
|
||||
f"{device.device.device_id}_{MAIN}"
|
||||
f"_{Capability.CUSTOM_DO_NOT_DISTURB_MODE}"
|
||||
f"_{entity_description.attribute}"
|
||||
f"_{entity_description.attribute}"
|
||||
)
|
||||
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Set the time value."""
|
||||
@@ -87,7 +92,9 @@ class SmartThingsDnDTime(SmartThingsEntity, TimeEntity):
|
||||
Command.SET_DO_NOT_DISTURB_MODE,
|
||||
{
|
||||
**payload,
|
||||
self.entity_description.attribute: f"{value.hour:02d}{value.minute:02d}",
|
||||
self.entity_description.attribute: (
|
||||
f"{value.hour:02d}{value.minute:02d}"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -93,7 +93,10 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class SmartTubOnline(SmartTubOnboardSensorBase, BinarySensorEntity):
|
||||
"""A binary sensor indicating whether the spa is currently online (connected to the cloud)."""
|
||||
"""A binary sensor indicating whether the spa is online.
|
||||
|
||||
Indicates if it is connected to the cloud.
|
||||
"""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
||||
# This seems to be very noisy and not generally useful, so disable by default.
|
||||
|
||||
@@ -101,5 +101,5 @@ class SmartTubExternalSensorBase(SmartTubEntity):
|
||||
|
||||
@property
|
||||
def sensor(self) -> SpaSensor:
|
||||
"""Convenience property to access the smarttub.SpaSensor instance for this sensor."""
|
||||
"""Access the smarttub.SpaSensor instance for this sensor."""
|
||||
return self.coordinator.data[self.spa.id][ATTR_SENSORS][self.sensor_address]
|
||||
|
||||
@@ -23,7 +23,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: SMHIConfigEntry) -> bool
|
||||
|
||||
# Setting unique id where missing
|
||||
if entry.unique_id is None:
|
||||
unique_id = f"{entry.data[CONF_LOCATION][CONF_LATITUDE]}-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}"
|
||||
unique_id = (
|
||||
f"{entry.data[CONF_LOCATION][CONF_LATITUDE]}"
|
||||
f"-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}"
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, unique_id=unique_id)
|
||||
|
||||
coordinator = SMHIDataUpdateCoordinator(hass, entry)
|
||||
|
||||
@@ -25,5 +25,8 @@ class SmEntity(CoordinatorEntity[SmBaseDataUpdateCoordinator]):
|
||||
connections={(CONNECTION_NETWORK_MAC, mac)},
|
||||
manufacturer=ATTR_MANUFACTURER,
|
||||
model=coordinator.data.info.model,
|
||||
sw_version=f"core: {coordinator.data.info.sw_version} / zigbee: {coordinator.data.info.zb_version}",
|
||||
sw_version=(
|
||||
f"core: {coordinator.data.info.sw_version}"
|
||||
f" / zigbee: {coordinator.data.info.zb_version}"
|
||||
),
|
||||
)
|
||||
|
||||
@@ -249,7 +249,7 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
|
||||
@property
|
||||
def group_members(self) -> list[str] | None:
|
||||
"""List of player entities which are currently grouped together for synchronous playback."""
|
||||
"""List of players currently grouped for synchronous playback."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
@@ -298,7 +298,8 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
# Validate client belongs to the same server
|
||||
if not client.unique_id.startswith(unique_id_prefix):
|
||||
raise ServiceValidationError(
|
||||
f"Entity '{client.entity_id}' does not belong to the same Snapcast server."
|
||||
f"Entity '{client.entity_id}' does not belong"
|
||||
" to the same Snapcast server."
|
||||
)
|
||||
|
||||
# Extract client ID and join it to the current group
|
||||
@@ -307,7 +308,8 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
await self._current_group.add_client(identifier)
|
||||
except KeyError as e:
|
||||
raise ServiceValidationError(
|
||||
f"Client with identifier '{identifier}' does not exist on the server."
|
||||
f"Client with identifier '{identifier}'"
|
||||
" does not exist on the server."
|
||||
) from e
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -102,7 +102,7 @@ class SnmpScanner(DeviceScanner):
|
||||
|
||||
@classmethod
|
||||
async def create(cls, config):
|
||||
"""Asynchronously test the target device before fully initializing the scanner."""
|
||||
"""Test the target device before fully initializing."""
|
||||
host = config[CONF_HOST]
|
||||
|
||||
try:
|
||||
@@ -125,7 +125,7 @@ class SnmpScanner(DeviceScanner):
|
||||
return instance
|
||||
|
||||
async def async_init(self, hass: HomeAssistant) -> None:
|
||||
"""Make a one-off read to check if the target device is reachable and readable."""
|
||||
"""Check if the target device is reachable and readable."""
|
||||
self.request_args = await async_create_request_cmd_args(
|
||||
hass,
|
||||
self._auth_data,
|
||||
@@ -146,7 +146,7 @@ class SnmpScanner(DeviceScanner):
|
||||
return None
|
||||
|
||||
async def async_get_extra_attributes(self, device: str) -> dict:
|
||||
"""Return the extra attributes of the given device or an empty dictionary if we have none."""
|
||||
"""Return extra attributes of the given device."""
|
||||
for client in self.last_results:
|
||||
if client.get("mac") and device == client["mac"]:
|
||||
return {"mac": client["mac"]}
|
||||
|
||||
@@ -115,7 +115,8 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService):
|
||||
data = value
|
||||
self.data[key] = data
|
||||
|
||||
# Sanity check the energy values. SolarEdge API sometimes report "lifetimedata" of zero,
|
||||
# Sanity check the energy values. SolarEdge API sometimes
|
||||
# reports "lifetimedata" of zero,
|
||||
# while values for last Year, Month and Day energy are still OK.
|
||||
# See https://github.com/home-assistant/core/issues/59285 .
|
||||
if set(energy_keys).issubset(self.data.keys()):
|
||||
@@ -453,8 +454,9 @@ class SolarEdgeModulesCoordinator(DataUpdateCoordinator[None]):
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint and update statistics."""
|
||||
equipment: dict[int, dict[str, Any]] = await self.api.async_get_equipment()
|
||||
# We fetch last week's data from the API and refresh every 12h so we overwrite recent
|
||||
# statistics. This is intended to allow adding any corrected/updated data from the API.
|
||||
# We fetch last week's data from the API and refresh
|
||||
# every 12h so we overwrite recent statistics. This is
|
||||
# intended to allow adding any corrected/updated data.
|
||||
energy_data_list: list[EnergyData] = await self.api.async_get_energy_data(
|
||||
TimeUnit.WEEK
|
||||
)
|
||||
@@ -545,9 +547,10 @@ class SolarEdgeModulesCoordinator(DataUpdateCoordinator[None]):
|
||||
if statistic_id in current_stats:
|
||||
statistic_sum = current_stats[statistic_id][0]["sum"]
|
||||
else:
|
||||
# If no statistics found right before start_time, try to get the last statistic
|
||||
# but use it only if it's before start_time.
|
||||
# This is needed if the integration hasn't run successfully for at least a week.
|
||||
# If no statistics found right before start_time,
|
||||
# try to get the last statistic but use it only
|
||||
# if it's before start_time. This is needed if
|
||||
# the integration hasn't run for at least a week.
|
||||
last_stat = await get_instance(self.hass).async_add_executor_job(
|
||||
get_last_statistics, self.hass, 1, statistic_id, True, {"sum"}
|
||||
)
|
||||
|
||||
@@ -74,7 +74,8 @@ class SolarLogBasicDataCoordinator(DataUpdateCoordinator[SolarlogData]):
|
||||
) from ex
|
||||
except SolarLogAuthenticationError as ex:
|
||||
if await self.renew_authentication():
|
||||
# login was successful, update availability of extended data, retry data update
|
||||
# login was successful, update availability
|
||||
# of extended data, retry data update
|
||||
await self.solarlog.test_extended_data_available()
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -258,7 +259,9 @@ class SolarLogLongtimeDataCoordinator(DataUpdateCoordinator[EnergyData]):
|
||||
if energy_data is None:
|
||||
energy_data = EnergyData(None, None)
|
||||
|
||||
self.config_entry.runtime_data.basic_data_coordinator.data.self_consumption_year = energy_data.self_consumption
|
||||
(
|
||||
self.config_entry.runtime_data.basic_data_coordinator.data.self_consumption_year
|
||||
) = energy_data.self_consumption
|
||||
|
||||
_LOGGER.debug("Energy data successfully updated")
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ class SolarLogInverterEntity(CoordinatorEntity[SolarLogDeviceDataCoordinator]):
|
||||
) -> None:
|
||||
"""Initialize the SolarLogInverter sensor."""
|
||||
super().__init__(coordinator)
|
||||
name = f"{coordinator.config_entry.entry_id}_{slugify(coordinator.solarlog.device_name(device_id))}"
|
||||
device_name = coordinator.solarlog.device_name(device_id)
|
||||
name = f"{coordinator.config_entry.entry_id}_{slugify(device_name)}"
|
||||
self._attr_unique_id = f"{name}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
manufacturer="Solar-Log",
|
||||
|
||||
@@ -261,8 +261,9 @@ SOLARLOG_BASIC_SENSOR_TYPES: tuple[SolarLogCoordinatorSensorEntityDescription, .
|
||||
),
|
||||
)
|
||||
|
||||
"""SOLARLOG_LONGTIME_SENSOR_TYPES represent data points that may require longer timeout and
|
||||
therefore are retrieved with different DataUpdateCoordinator."""
|
||||
"""SOLARLOG_LONGTIME_SENSOR_TYPES represent data points that
|
||||
may require longer timeout and therefore are retrieved with
|
||||
different DataUpdateCoordinator."""
|
||||
SOLARLOG_LONGTIME_SENSOR_TYPES: tuple[SolarLogLongtimeSensorEntityDescription, ...] = (
|
||||
SolarLogLongtimeSensorEntityDescription(
|
||||
key="self_consumption_year",
|
||||
@@ -351,7 +352,8 @@ async def async_setup_entry(
|
||||
for sensor in SOLARLOG_LONGTIME_SENSOR_TYPES
|
||||
)
|
||||
|
||||
# add battery sensors only if respective data is available (otherwise no battery attached to solarlog)
|
||||
# add battery sensors only if respective data is
|
||||
# available (otherwise no battery attached to solarlog)
|
||||
if solarLogIntegrationData.basic_data_coordinator.data.battery_data is not None:
|
||||
entities.extend(
|
||||
SolarLogBatterySensor(
|
||||
|
||||
@@ -30,7 +30,8 @@ async def async_setup_entry(
|
||||
entities: list[SomaTilt | SomaShade] = []
|
||||
|
||||
for device in data.devices:
|
||||
# Assume a shade device if the type is not present in the api response (Connect <2.2.6)
|
||||
# Assume a shade device if the type is not present
|
||||
# in the api response (Connect <2.2.6)
|
||||
if "type" in device and device["type"].lower() == "tilt":
|
||||
entities.append(SomaTilt(device, api))
|
||||
else:
|
||||
|
||||
@@ -46,7 +46,8 @@ def format_queue_item(item: Any, base_url: str | None = None) -> dict[str, Any]:
|
||||
if episode := getattr(item, "episode", None):
|
||||
result["episode_number"] = getattr(episode, "episodeNumber", None)
|
||||
result["episode_title"] = getattr(episode, "title", None)
|
||||
# Add formatted identifier like the sensor uses (if we have both season and episode)
|
||||
# Add formatted identifier like the sensor uses
|
||||
# (if we have both season and episode)
|
||||
if result["season_number"] is not None and result["episode_number"] is not None:
|
||||
result["episode_identifier"] = (
|
||||
f"S{result['season_number']:02d}E{result['episode_number']:02d}"
|
||||
@@ -197,7 +198,8 @@ def format_diskspace(
|
||||
|
||||
Args:
|
||||
disks: List of disk space objects from Sonarr.
|
||||
space_unit: Unit for space values (bytes, kb, kib, mb, mib, gb, gib, tb, tib, pb, pib).
|
||||
space_unit: Unit for space values
|
||||
(bytes, kb, kib, mb, mib, gb, gib, tb, tib, pb, pib).
|
||||
|
||||
Returns:
|
||||
Dictionary of disk information keyed by path.
|
||||
@@ -244,7 +246,9 @@ def format_upcoming_item(
|
||||
"series_id": episode.seriesId,
|
||||
"season_number": episode.seasonNumber,
|
||||
"episode_number": episode.episodeNumber,
|
||||
"episode_identifier": f"S{episode.seasonNumber:02d}E{episode.episodeNumber:02d}",
|
||||
"episode_identifier": (
|
||||
f"S{episode.seasonNumber:02d}E{episode.episodeNumber:02d}"
|
||||
),
|
||||
"title": episode.title,
|
||||
"air_date": str(getattr(episode, "airDate", None)),
|
||||
"air_date_utc": str(getattr(episode, "airDateUtc", None)),
|
||||
@@ -341,7 +345,9 @@ def format_episode(episode: SonarrEpisode) -> dict[str, Any]:
|
||||
"tvdb_id": getattr(episode, "tvdbId", None),
|
||||
"season_number": episode.seasonNumber,
|
||||
"episode_number": episode.episodeNumber,
|
||||
"episode_identifier": f"S{episode.seasonNumber:02d}E{episode.episodeNumber:02d}",
|
||||
"episode_identifier": (
|
||||
f"S{episode.seasonNumber:02d}E{episode.episodeNumber:02d}"
|
||||
),
|
||||
"title": episode.title,
|
||||
"air_date": str(getattr(episode, "airDate", None)),
|
||||
"air_date_utc": str(getattr(episode, "airDateUtc", None)),
|
||||
|
||||
@@ -343,7 +343,8 @@ class SongpalEntity(MediaPlayerEntity):
|
||||
def sound_mode_list(self) -> list[str] | None:
|
||||
"""Return list of available sound modes.
|
||||
|
||||
When active mode is None it means that sound mode is unavailable on the sound bar.
|
||||
When active mode is None it means that sound mode is
|
||||
unavailable on the sound bar.
|
||||
Can be due to incompatible sound bar or the sound bar is in a mode that does not
|
||||
support sound mode changes.
|
||||
"""
|
||||
|
||||
@@ -218,13 +218,15 @@ class SonosDiscoveryManager:
|
||||
_ = IPv4Address(ip_address)
|
||||
except AddressValueError:
|
||||
_LOGGER.debug(
|
||||
"Sonos integration only supports IPv4 addresses, invalid ip_address received: %s",
|
||||
"Sonos integration only supports IPv4 addresses,"
|
||||
" invalid ip_address received: %s",
|
||||
ip_address,
|
||||
)
|
||||
return
|
||||
soco = SoCo(ip_address)
|
||||
try:
|
||||
# Cache now to avoid household ID lookup during first ZoneGroupState processing
|
||||
# Cache now to avoid household ID lookup during
|
||||
# first ZoneGroupState processing
|
||||
await self.hass.async_add_executor_job(
|
||||
getattr,
|
||||
soco,
|
||||
@@ -426,7 +428,8 @@ class SonosDiscoveryManager:
|
||||
) -> None:
|
||||
"""Add and maintain Sonos devices from a manual configuration."""
|
||||
|
||||
# Loop through each configured host and verify that Soco attributes are available for it.
|
||||
# Loop through each configured host and verify that
|
||||
# Soco attributes are available for it.
|
||||
for host in self.hosts.copy():
|
||||
ip_addr = await self.hass.async_add_executor_job(socket.gethostbyname, host)
|
||||
soco = SoCo(ip_addr)
|
||||
@@ -457,8 +460,9 @@ class SonosDiscoveryManager:
|
||||
|
||||
if self.hosts_in_error.pop(ip_addr, None):
|
||||
_LOGGER.warning("Connection reestablished to Sonos device %s", ip_addr)
|
||||
# Each speaker has the topology for other online speakers, so add them in here if they were not
|
||||
# configured. The metadata is already in Soco for these.
|
||||
# Each speaker has the topology for other online
|
||||
# speakers, so add them in here if they were not
|
||||
# configured. The metadata is already in Soco.
|
||||
if new_hosts := {
|
||||
x.ip_address for x in visible_zones if x.ip_address not in self.hosts
|
||||
}:
|
||||
@@ -469,12 +473,14 @@ class SonosDiscoveryManager:
|
||||
_LOGGER.debug("Discarding %s from manual hosts", ip_addr)
|
||||
self.hosts.discard(ip_addr)
|
||||
|
||||
# Loop through each configured host that is not in error. Send a discovery message
|
||||
# if a speaker does not already exist, or ping the speaker if it is unavailable.
|
||||
# Loop through each configured host that is not in
|
||||
# error. Send a discovery message if a speaker does
|
||||
# not already exist, or ping if it is unavailable.
|
||||
for host in self.hosts.copy():
|
||||
ip_addr = await self.hass.async_add_executor_job(socket.gethostbyname, host)
|
||||
soco = SoCo(ip_addr)
|
||||
# Skip hosts that are in error to avoid blocking call on soco.uuid in event loop
|
||||
# Skip hosts that are in error to avoid blocking
|
||||
# call on soco.uuid in event loop
|
||||
if self.hosts_in_error.get(ip_addr):
|
||||
continue
|
||||
known_speaker = next(
|
||||
|
||||
@@ -89,10 +89,14 @@ class SonosAlarms(SonosHouseholdCoordinator):
|
||||
if "Alarm list UID" in err_msg and "does not match" in err_msg:
|
||||
if not self._household_mismatch_logged:
|
||||
_LOGGER.warning(
|
||||
"Sonos alarms for %s cannot be updated due to a household mismatch. "
|
||||
"This is a known limitation in setups with multiple households. "
|
||||
"You can safely ignore this warning, or to silence it, remove the "
|
||||
"affected household from your Sonos system. Error: %s",
|
||||
"Sonos alarms for %s cannot be updated"
|
||||
" due to a household mismatch. "
|
||||
"This is a known limitation in setups"
|
||||
" with multiple households. "
|
||||
"You can safely ignore this warning,"
|
||||
" or to silence it, remove the "
|
||||
"affected household from your Sonos"
|
||||
" system. Error: %s",
|
||||
soco.player_name,
|
||||
err_msg,
|
||||
)
|
||||
|
||||
@@ -107,7 +107,7 @@ class SonosEntity(Entity):
|
||||
|
||||
|
||||
class SonosPollingEntity(SonosEntity):
|
||||
"""Representation of a Sonos entity which may not support updating by subscriptions."""
|
||||
"""Representation of a Sonos entity without subscription support."""
|
||||
|
||||
@abstractmethod
|
||||
def poll_state(self) -> None:
|
||||
|
||||
@@ -93,7 +93,7 @@ def soco_error[_T: _SonosEntitiesType, **_P, _R](
|
||||
|
||||
|
||||
def _find_target_identifier(instance: Any, fallback_soco: SoCo | None) -> str | None:
|
||||
"""Extract the best available target identifier from the provided instance object."""
|
||||
"""Extract the best target identifier from the instance."""
|
||||
if entity_id := getattr(instance, "entity_id", None):
|
||||
# SonosEntity instance
|
||||
return entity_id
|
||||
|
||||
@@ -81,7 +81,10 @@ class SonosHouseholdCoordinator:
|
||||
raise NotImplementedError
|
||||
|
||||
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
|
||||
"""Update the cache of the household-level feature and return if cache has changed."""
|
||||
"""Update the household-level feature cache.
|
||||
|
||||
Return if cache has changed.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def add_speaker(self, soco: SoCo) -> None:
|
||||
|
||||
@@ -46,9 +46,11 @@ type GetBrowseImageUrlType = Callable[[str, str, str | None], str]
|
||||
|
||||
|
||||
def fix_image_url(url: str) -> str:
|
||||
"""Update the image url to fully encode characters to allow image display in media_browser UI.
|
||||
"""Update the image url to fully encode characters.
|
||||
|
||||
Images whose file path contains characters such as ',()+ are not loaded without escaping them.
|
||||
This allows image display in media_browser UI. Images
|
||||
whose file path contains characters such as ',()+ are
|
||||
not loaded without escaping them.
|
||||
"""
|
||||
|
||||
# Before parsing encode the plus sign; otherwise it'll be interpreted as a space.
|
||||
@@ -108,7 +110,8 @@ def _get_title(id_string: str) -> str:
|
||||
"""Extract a suitable title from the content id string."""
|
||||
if id_string.startswith("S:"):
|
||||
# Format is S://server/share/folder
|
||||
# If just S: this will be in the mappings; otherwise use the last folder in path.
|
||||
# If just S: this will be in the mappings;
|
||||
# otherwise use the last folder in path.
|
||||
title = LIBRARY_TITLES_MAPPING.get(
|
||||
id_string, urllib.parse.unquote(id_string.rsplit("/", maxsplit=1)[-1])
|
||||
)
|
||||
@@ -621,9 +624,10 @@ def get_media(
|
||||
)
|
||||
matches = [result]
|
||||
else:
|
||||
# When requesting media by album_artist, composer, genre use the browse interface
|
||||
# to navigate the hierarchy. This occurs when invoked from media browser or service
|
||||
# calls
|
||||
# When requesting media by album_artist, composer,
|
||||
# genre use the browse interface to navigate the
|
||||
# hierarchy. This occurs when invoked from media
|
||||
# browser or service calls
|
||||
# Example: A:ALBUMARTIST/Neil Young/Greatest Hits - get specific album
|
||||
# Example: A:ALBUMARTIST/Neil Young - get all albums
|
||||
# Others: composer, genre
|
||||
|
||||
@@ -839,15 +839,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
async def async_unjoin_player(self) -> None:
|
||||
"""Remove this player from any group.
|
||||
|
||||
Coalesces all calls within UNJOIN_SERVICE_TIMEOUT to allow use of SonosSpeaker.unjoin_multi()
|
||||
which optimizes the order in which speakers are removed from their groups.
|
||||
Removing coordinators last better preserves playqueues on the speakers.
|
||||
Coalesces all calls within UNJOIN_SERVICE_TIMEOUT to
|
||||
allow use of SonosSpeaker.unjoin_multi() which
|
||||
optimizes the order in which speakers are removed
|
||||
from their groups. Removing coordinators last better
|
||||
preserves playqueues on the speakers.
|
||||
"""
|
||||
sonos_data = self.config_entry.runtime_data
|
||||
household_id = self.speaker.household_id
|
||||
|
||||
async def async_process_unjoin(now: datetime.datetime) -> None:
|
||||
"""Process the unjoin with all remove requests within the coalescing period."""
|
||||
"""Process the unjoin with all remove requests."""
|
||||
unjoin_data = sonos_data.unjoin_data.pop(household_id)
|
||||
_LOGGER.debug(
|
||||
"Processing unjoins for %s", [x.zone_name for x in unjoin_data.speakers]
|
||||
|
||||
@@ -351,7 +351,7 @@ class SonosSpeaker:
|
||||
def log_subscription_result(
|
||||
self, result: Any, event: str, level: int = logging.DEBUG
|
||||
) -> None:
|
||||
"""Log a message if a subscription action (create/renew/stop) results in an exception."""
|
||||
"""Log if a subscription action results in an exception."""
|
||||
if not isinstance(result, Exception):
|
||||
return
|
||||
|
||||
@@ -966,7 +966,8 @@ class SonosSpeaker:
|
||||
new_members = set(sonos_group[1:])
|
||||
removed_members = old_members - new_members
|
||||
for removed_speaker in removed_members:
|
||||
# Only clear if this speaker was coordinated by self and in the same group
|
||||
# Only clear if this speaker was coordinated
|
||||
# by self and in the same group
|
||||
if (
|
||||
removed_speaker.coordinator == self
|
||||
and removed_speaker.sonos_group is self.sonos_group
|
||||
@@ -1176,10 +1177,12 @@ class SonosSpeaker:
|
||||
if not with_group:
|
||||
return groups
|
||||
|
||||
# Unjoin non-coordinator speakers not contained in the desired snapshot group
|
||||
# Unjoin non-coordinator speakers not contained in
|
||||
# the desired snapshot group
|
||||
#
|
||||
# If a coordinator is unjoined from its group, another speaker from the group
|
||||
# will inherit the coordinator's playqueue and its own playqueue will be lost
|
||||
# If a coordinator is unjoined from its group,
|
||||
# another speaker from the group will inherit the
|
||||
# coordinator's playqueue and its own will be lost
|
||||
speakers_to_unjoin = set()
|
||||
for speaker in speakers:
|
||||
if speaker.sonos_group == speaker.snapshot_group:
|
||||
@@ -1265,7 +1268,8 @@ class SonosSpeaker:
|
||||
await config_entry.runtime_data.topology_condition.wait()
|
||||
except TimeoutError:
|
||||
group_description = "; ".join(
|
||||
f"{group[0].zone_name}: {', '.join(speaker.zone_name for speaker in group)}"
|
||||
f"{group[0].zone_name}: "
|
||||
f"{', '.join(speaker.zone_name for speaker in group)}"
|
||||
for group in groups
|
||||
)
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -155,7 +155,7 @@ async def async_setup_entry(
|
||||
def _get_switch_state(
|
||||
speaker: SonosSpeaker,
|
||||
) -> tuple[list[str], str | None, bool | None]:
|
||||
"""Return all switch state needed for entity creation in a single executor call."""
|
||||
"""Return all switch state for entity creation."""
|
||||
return (
|
||||
available_soco_attributes(speaker),
|
||||
_get_tv_autoplay_state(speaker),
|
||||
@@ -399,7 +399,8 @@ class SonosTVUngroupAutoplaySwitchEntity(SonosPollingEntity, SwitchEntity):
|
||||
"""Enable or disable ungroup on autoplay on the device."""
|
||||
try:
|
||||
self.soco.deviceProperties.SetAutoplayLinkedZones(
|
||||
# enable=True (ungroup) → IncludeLinkedZones=0 (don't include linked zones)
|
||||
# enable=True (ungroup) → IncludeLinkedZones=0
|
||||
# (don't include linked zones)
|
||||
[("IncludeLinkedZones", "0" if enable else "1"), *_TV_SOURCE]
|
||||
)
|
||||
except SoCoUPnPException as exc:
|
||||
|
||||
@@ -393,7 +393,7 @@ class SoundTouchMediaPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
|
||||
def _get_instance_by_id(self, instance_id):
|
||||
"""Search and return a SoundTouchDevice instance by it's ID (aka MAC address)."""
|
||||
"""Search and return a SoundTouchDevice by its ID."""
|
||||
for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
data = entry.runtime_data
|
||||
if data.device.config.device_id == instance_id:
|
||||
|
||||
@@ -121,8 +121,9 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
|
||||
position_updated_at=None,
|
||||
playlist=None,
|
||||
)
|
||||
# Record the last updated time, because Spotify's timestamp property is unreliable
|
||||
# and doesn't actually return the fetch time as is mentioned in the API description
|
||||
# Record the last updated time, because Spotify's
|
||||
# timestamp property is unreliable and doesn't
|
||||
# actually return the fetch time as described in API
|
||||
position_updated_at = dt_util.utcnow()
|
||||
|
||||
dj_playlist = False
|
||||
|
||||
@@ -121,7 +121,8 @@ def validate_query(
|
||||
Args:
|
||||
hass: The Home Assistant instance.
|
||||
query_template: The SQL query string to be validated.
|
||||
uses_recorder_db: A boolean indicating if the query is against the recorder database.
|
||||
uses_recorder_db: A boolean indicating if the query is
|
||||
against the recorder database.
|
||||
unique_id: The unique ID of the entity, used for creating issue registry keys.
|
||||
|
||||
Raises:
|
||||
|
||||
@@ -140,7 +140,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
|
||||
},
|
||||
)
|
||||
|
||||
# For other errors where status is None (e.g., server error, connection refused by server)
|
||||
# For other errors where status is None
|
||||
# (e.g., server error, connection refused by server)
|
||||
_LOGGER.warning(
|
||||
"LMS %s returned no status or an error (HTTP status: %s). Retrying setup",
|
||||
host,
|
||||
|
||||
@@ -324,10 +324,12 @@ async def build_item_response(
|
||||
child_media = _build_response_favorites(item)
|
||||
|
||||
elif search_type in ["apps", "radios"]:
|
||||
# item["cmd"] contains the name of the command to use with the cli for the app
|
||||
# item["cmd"] contains the name of the command
|
||||
# to use with the cli for the app;
|
||||
# add the command to the dictionaries
|
||||
if item["title"] == "Search" or item.get("type") in UNPLAYABLE_TYPES:
|
||||
# Skip searches in apps as they'd need UI or if the link isn't to audio
|
||||
# Skip searches in apps as they'd need UI
|
||||
# or if the link isn't to audio
|
||||
continue
|
||||
app_cmd = "app-" + item["cmd"]
|
||||
|
||||
|
||||
@@ -287,7 +287,10 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="edit_integration_discovered",
|
||||
description_placeholders={
|
||||
"desc": f"LMS Host: {self.chosen_server[CONF_HOST]}, Port: {self.chosen_server[CONF_PORT]}"
|
||||
"desc": (
|
||||
f"LMS Host: {self.chosen_server[CONF_HOST]},"
|
||||
f" Port: {self.chosen_server[CONF_PORT]}"
|
||||
)
|
||||
},
|
||||
data_schema=SHORT_EDIT_SCHEMA,
|
||||
errors=errors,
|
||||
@@ -330,7 +333,10 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self.unique_id
|
||||
# if we have detected this player, do nothing. if not, there must be a server out there for us to configure, so start the normal user flow (which tries to autodetect server)
|
||||
# if we have detected this player, do nothing. if not,
|
||||
# there must be a server out there for us to configure,
|
||||
# so start the normal user flow (which tries to
|
||||
# autodetect server)
|
||||
if registry.async_get_entity_id(MP_DOMAIN, DOMAIN, self.unique_id) is not None:
|
||||
# this player is already known, so do nothing other than mark as configured
|
||||
raise AbortFlow("already_configured")
|
||||
|
||||
@@ -106,7 +106,8 @@ class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update the Player() object if available, or listen for rediscovery if not."""
|
||||
if self.available:
|
||||
# Only update players available at last update, unavailable players are rediscovered instead
|
||||
# Only update players available at last update,
|
||||
# unavailable players are rediscovered instead
|
||||
await self.player.async_update()
|
||||
|
||||
if not self.player.connected:
|
||||
|
||||
@@ -32,8 +32,12 @@ class SqueezeboxEntity(CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator]):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
# super().available refers to CoordinatorEntity.available (self.coordinator.last_update_success)
|
||||
# self.coordinator.available is the custom availability flag from SqueezeBoxPlayerUpdateCoordinator
|
||||
# super().available refers to
|
||||
# CoordinatorEntity.available
|
||||
# (self.coordinator.last_update_success).
|
||||
# self.coordinator.available is the custom
|
||||
# availability flag from
|
||||
# SqueezeBoxPlayerUpdateCoordinator
|
||||
return self.coordinator.available and super().available
|
||||
|
||||
|
||||
|
||||
@@ -134,7 +134,8 @@ async def async_setup_entry(
|
||||
manufacturer = player.creator
|
||||
model_id = player.model_type
|
||||
sw_version = ""
|
||||
# Why? so we nicely merge with a server and a player linked by a MAC server is not all info lost
|
||||
# So we nicely merge with a server and a player
|
||||
# linked by a MAC server is not all info lost
|
||||
if (
|
||||
server_device
|
||||
and (CONNECTION_NETWORK_MAC, format_mac(player.player_id))
|
||||
@@ -762,8 +763,8 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
async def async_join_players(self, group_members: list[str]) -> None:
|
||||
"""Add other Squeezebox players to this player's sync group.
|
||||
|
||||
If the other player is a member of a sync group, it will leave the current sync group
|
||||
without asking.
|
||||
If the other player is a member of a sync group,
|
||||
it will leave the current sync group without asking.
|
||||
"""
|
||||
ent_reg = er.async_get(self.hass)
|
||||
for other_player_entity_id in group_members:
|
||||
@@ -798,7 +799,8 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
def get_synthetic_id_and_cache_url(self, url: str) -> str:
|
||||
"""Cache a thumbnail URL and return a synthetic ID.
|
||||
|
||||
This enables us to proxy thumbnails for apps and favorites, as those do not have IDs.
|
||||
This enables us to proxy thumbnails for apps and
|
||||
favorites, as those do not have IDs.
|
||||
"""
|
||||
synthetic_id = f"s_{ulid_now()}"
|
||||
|
||||
|
||||
@@ -81,10 +81,12 @@ async def async_setup_entry(
|
||||
coordinator.async_add_listener(_async_listener)
|
||||
|
||||
# If coordinator already has alarm data from the initial refresh,
|
||||
# call the listener immediately to process existing alarms and create alarm entities.
|
||||
# call the listener immediately to process existing
|
||||
# alarms and create alarm entities.
|
||||
if coordinator.data["alarms"]:
|
||||
_LOGGER.debug(
|
||||
"Coordinator has alarm data, calling _async_listener immediately for player %s",
|
||||
"Coordinator has alarm data, calling"
|
||||
" _async_listener immediately for player %s",
|
||||
coordinator.player,
|
||||
)
|
||||
_async_listener()
|
||||
|
||||
@@ -115,8 +115,10 @@ class ServerStatusUpdatePlugins(ServerStatusUpdate):
|
||||
"""If install is supported give some info."""
|
||||
rs = self.coordinator.data[UPDATE_PLUGINS_RELEASE_SUMMARY]
|
||||
return (
|
||||
(rs or "")
|
||||
+ "The Plugins will be updated on the next restart triggered by selecting the Update button. Allow enough time for the service to restart. It will become briefly unavailable."
|
||||
(rs or "") + "The Plugins will be updated on the next restart"
|
||||
" triggered by selecting the Update button."
|
||||
" Allow enough time for the service to restart."
|
||||
" It will become briefly unavailable."
|
||||
if self.coordinator.can_server_restart
|
||||
else rs
|
||||
)
|
||||
|
||||
@@ -388,7 +388,8 @@ class Scanner:
|
||||
ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source]
|
||||
_async_process_callbacks(self.hass, callbacks, discovery_info, ssdp_change)
|
||||
|
||||
# Config flows should only be created for alive/update messages from alive devices
|
||||
# Config flows should only be created for alive/update
|
||||
# messages from alive devices
|
||||
if source == SsdpSource.ADVERTISEMENT_BYEBYE:
|
||||
self._async_dismiss_discoveries(discovery_info)
|
||||
return
|
||||
|
||||
@@ -124,7 +124,11 @@ class Server:
|
||||
async def _async_get_instance_udn(self) -> str:
|
||||
"""Get Unique Device Name for this instance."""
|
||||
instance_id = await async_get_instance_id(self.hass)
|
||||
return f"uuid:{instance_id[0:8]}-{instance_id[8:12]}-{instance_id[12:16]}-{instance_id[16:20]}-{instance_id[20:32]}".upper()
|
||||
return (
|
||||
f"uuid:{instance_id[0:8]}-{instance_id[8:12]}"
|
||||
f"-{instance_id[12:16]}-{instance_id[16:20]}"
|
||||
f"-{instance_id[20:32]}"
|
||||
).upper()
|
||||
|
||||
async def _async_start_upnp_servers(self, event: Event) -> None:
|
||||
"""Start the UPnP/SSDP servers."""
|
||||
|
||||
@@ -60,7 +60,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="fuel",
|
||||
translation_key="fuel",
|
||||
# No device_class: fuel can be reported as percentage or volume depending on vehicle
|
||||
# No device_class: fuel can be reported as percentage
|
||||
# or volume depending on vehicle
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
|
||||
@@ -140,7 +140,8 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
|
||||
"""Set Starlink system sleep schedule end time."""
|
||||
duration = end - self.data.sleep[0]
|
||||
if duration < 0:
|
||||
# If the duration pushed us into the next day, add one days worth to correct that.
|
||||
# If the duration pushed us into the next day,
|
||||
# add one days worth to correct that.
|
||||
duration += 1440
|
||||
async with asyncio.timeout(4):
|
||||
try:
|
||||
|
||||
@@ -64,7 +64,7 @@ class StarlinkSensorEntity(StarlinkEntity, SensorEntity):
|
||||
|
||||
|
||||
class StarlinkAccumulationSensor(StarlinkSensorEntity, RestoreSensor):
|
||||
"""A StarlinkAccumulationSensor for Starlink devices. Handles creating unique IDs."""
|
||||
"""A StarlinkAccumulationSensor for Starlink devices."""
|
||||
|
||||
_attr_native_value: int | float = 0
|
||||
|
||||
|
||||
@@ -525,7 +525,7 @@ ICON = "mdi:calculator"
|
||||
|
||||
|
||||
def valid_state_characteristic_configuration(config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate that the characteristic selected is valid for the source sensor type, throw if it isn't."""
|
||||
"""Validate characteristic is valid for source sensor type."""
|
||||
is_binary = split_entity_id(config[CONF_ENTITY_ID])[0] == BINARY_SENSOR_DOMAIN
|
||||
characteristic = cast(str, config[CONF_STATE_CHARACTERISTIC])
|
||||
if (is_binary and characteristic not in STATS_BINARY_SUPPORT) or (
|
||||
@@ -556,7 +556,8 @@ def valid_keep_last_sample(config: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
if config.get(CONF_KEEP_LAST_SAMPLE) is True and config.get(CONF_MAX_AGE) is None:
|
||||
raise vol.RequiredFieldInvalid(
|
||||
"The sensor configuration must provide 'max_age' if 'keep_last_sample' is True"
|
||||
"The sensor configuration must provide 'max_age'"
|
||||
" if 'keep_last_sample' is True"
|
||||
)
|
||||
return config
|
||||
|
||||
@@ -932,7 +933,8 @@ class StatisticsSensor(SensorEntity):
|
||||
|
||||
while self.ages and (now_timestamp - self.ages[0]) > max_age:
|
||||
if self.samples_keep_last and len(self.ages) == 1:
|
||||
# Under normal circumstance this will not be executed, as a purge will not
|
||||
# Under normal circumstance this will not be
|
||||
# executed, as a purge will not
|
||||
# be scheduled for the last value if samples_keep_last is enabled.
|
||||
# If this happens to be called outside normal scheduling logic or a
|
||||
# source sensor update, this ensures the last value is preserved.
|
||||
@@ -1096,7 +1098,8 @@ class StatisticsSensor(SensorEntity):
|
||||
def _update_value(self) -> None:
|
||||
"""Front to call the right statistical characteristics functions.
|
||||
|
||||
One of the _stat_*() functions is represented by self._state_characteristic_fn().
|
||||
One of the _stat_*() functions is represented by
|
||||
self._state_characteristic_fn().
|
||||
"""
|
||||
|
||||
value = self._state_characteristic_fn(self.states, self.ages, self._percentile)
|
||||
|
||||
@@ -185,8 +185,10 @@ def create_stream(
|
||||
) -> Stream:
|
||||
"""Create a stream with the specified identifier based on the source url.
|
||||
|
||||
The stream_source is typically an rtsp url (though any url accepted by ffmpeg is fine) and
|
||||
options (see STREAM_OPTIONS_SCHEMA) are converted and passed into pyav / ffmpeg.
|
||||
The stream_source is typically an rtsp url (though any
|
||||
url accepted by ffmpeg is fine) and options (see
|
||||
STREAM_OPTIONS_SCHEMA) are converted and passed into
|
||||
pyav / ffmpeg.
|
||||
|
||||
The stream_label is a string used as an additional message in logging.
|
||||
"""
|
||||
@@ -460,7 +462,8 @@ class Stream:
|
||||
|
||||
def _run_worker(self) -> None:
|
||||
"""Handle consuming streams and restart keepalive streams."""
|
||||
# Keep import here so that we can import stream integration without installing reqs
|
||||
# Keep import here so that we can import stream
|
||||
# integration without installing reqs
|
||||
from .worker import StreamState, stream_worker # noqa: PLC0415
|
||||
|
||||
stream_state = StreamState(self.hass, self.outputs, self._diagnostics)
|
||||
@@ -491,7 +494,8 @@ class Stream:
|
||||
stream_state.discontinuity()
|
||||
if not _should_retry() or self._thread_quit.is_set():
|
||||
if self._fast_restart_once:
|
||||
# The stream source is updated, restart without any delay and reset the retry
|
||||
# The stream source is updated, restart
|
||||
# without any delay and reset the retry
|
||||
# backoff for the new url.
|
||||
wait_timeout = 0
|
||||
self._fast_restart_once = False
|
||||
@@ -501,8 +505,9 @@ class Stream:
|
||||
|
||||
self._set_state(False)
|
||||
# To avoid excessive restarts, wait before restarting
|
||||
# As the required recovery time may be different for different setups, start
|
||||
# with trying a short wait_timeout and increase it on each reconnection attempt.
|
||||
# As the required recovery time may be different
|
||||
# for different setups, start with trying a short
|
||||
# wait_timeout and increase it on each attempt.
|
||||
# Reset the wait_timeout after the worker has been up for several minutes
|
||||
if time.time() - start_time > STREAM_RESTART_RESET_TIME:
|
||||
wait_timeout = 0
|
||||
@@ -515,9 +520,10 @@ class Stream:
|
||||
)
|
||||
|
||||
async def worker_finished() -> None:
|
||||
# The worker is no checking availability of the stream and can no longer track
|
||||
# availability so mark it as available, otherwise the frontend may not be able to
|
||||
# interact with the stream.
|
||||
# The worker is no longer checking availability
|
||||
# of the stream and can no longer track it so
|
||||
# mark it as available, otherwise the frontend
|
||||
# may not be able to interact with the stream.
|
||||
if not self.available:
|
||||
self._async_update_state(True)
|
||||
# We can call remove_provider() sequentially as the wrapped _stop() function
|
||||
@@ -555,7 +561,8 @@ class Stream:
|
||||
) -> None:
|
||||
"""Make a .mp4 recording from a provided stream."""
|
||||
|
||||
# Keep import here so that we can import stream integration without installing reqs
|
||||
# Keep import here so that we can import stream
|
||||
# integration without installing reqs
|
||||
from .recorder import RecorderOutput # noqa: PLC0415
|
||||
|
||||
# Check for file access
|
||||
|
||||
@@ -198,7 +198,7 @@ class Segment:
|
||||
def render_hls(
|
||||
self, last_stream_id: int, render_parts: bool, add_hint: bool
|
||||
) -> str:
|
||||
"""Render the HLS playlist section for the Segment including a hint if requested."""
|
||||
"""Render the Segment HLS playlist, optionally including parts and a hint."""
|
||||
playlist_template = self._render_hls_template(last_stream_id, render_parts)
|
||||
playlist = playlist_template.format(
|
||||
self.hls_playlist_parts[0] if render_parts else ""
|
||||
@@ -417,15 +417,17 @@ TRANSFORM_IMAGE_FUNCTION = (
|
||||
|
||||
|
||||
class KeyFrameConverter:
|
||||
"""Enables generating and getting an image from the last keyframe seen in the stream.
|
||||
"""Generate and get an image from the last keyframe.
|
||||
|
||||
An overview of the thread and state interaction:
|
||||
the worker thread sets a packet
|
||||
get_image is called from the main asyncio loop
|
||||
get_image schedules _generate_image in an executor thread
|
||||
_generate_image will try to create an image from the packet
|
||||
_generate_image will clear the packet, so there will only be one attempt per packet
|
||||
If successful, self._image will be updated and returned by get_image
|
||||
get_image schedules _generate_image in an executor
|
||||
_generate_image will try to create an image from
|
||||
the packet
|
||||
_generate_image will clear the packet, so there
|
||||
will only be one attempt per packet
|
||||
If successful, self._image will be updated and returned
|
||||
If unsuccessful, get_image will return the previous image
|
||||
"""
|
||||
|
||||
|
||||
@@ -119,8 +119,10 @@ class HlsMasterPlaylistView(StreamView):
|
||||
@staticmethod
|
||||
def render(track: StreamOutput) -> str:
|
||||
"""Render M3U8 file."""
|
||||
# Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work
|
||||
# Calculate file size / duration and use a small multiplier to account for variation
|
||||
# Need to calculate max bandwidth as
|
||||
# input_container.bit_rate doesn't seem to work.
|
||||
# Calculate file size / duration and use a small
|
||||
# multiplier to account for variation
|
||||
# hls spec already allows for 25% variation
|
||||
if not (segment := track.get_segment(track.sequences[-2])):
|
||||
return ""
|
||||
@@ -184,15 +186,15 @@ class HlsPlaylistView(StreamView):
|
||||
]
|
||||
|
||||
if track.stream_settings.ll_hls:
|
||||
part_dur = track.stream_settings.part_target_duration
|
||||
start_offset = EXT_X_START_LL_HLS * part_dur
|
||||
playlist.extend(
|
||||
[
|
||||
"#EXT-X-PART-INF:PART-TARGET="
|
||||
f"{track.stream_settings.part_target_duration:.3f}",
|
||||
"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK="
|
||||
f"{2 * track.stream_settings.part_target_duration:.3f}",
|
||||
"#EXT-X-START:TIME-OFFSET=-"
|
||||
f"{EXT_X_START_LL_HLS * track.stream_settings.part_target_duration:.3f}"
|
||||
",PRECISE=YES",
|
||||
f"#EXT-X-START:TIME-OFFSET=-{start_offset:.3f},PRECISE=YES",
|
||||
]
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -80,10 +80,12 @@ class RecorderOutput(StreamOutput):
|
||||
def write_segment(segment: Segment) -> None:
|
||||
"""Write a segment to output."""
|
||||
# fmt: off
|
||||
nonlocal output, output_v, output_a, last_stream_id, running_duration, last_sequence
|
||||
nonlocal output, output_v, output_a
|
||||
nonlocal last_stream_id, running_duration, last_sequence
|
||||
# fmt: on
|
||||
# Because the stream_worker is in a different thread from the record service,
|
||||
# the lookback segments may still have some overlap with the recorder segments
|
||||
# Because the stream_worker is in a different
|
||||
# thread from the record service, the lookback
|
||||
# segments may still overlap with recorder ones
|
||||
if segment.sequence <= last_sequence:
|
||||
return
|
||||
last_sequence = segment.sequence
|
||||
|
||||
@@ -185,7 +185,11 @@ class StreamMuxer:
|
||||
# https://github.com/home-assistant/core/pull/39970
|
||||
# "cmaf" flag replaces several of the movflags used,
|
||||
# but too recent to use for now
|
||||
"movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov",
|
||||
"movflags": (
|
||||
"frag_custom+empty_moov+default_base_moof"
|
||||
"+frag_discont+negative_cts_offsets"
|
||||
"+skip_trailer+delay_moov"
|
||||
),
|
||||
# Sometimes the first segment begins with negative timestamps,
|
||||
# and this setting just
|
||||
# adjusts the timestamps in the output from that segment to start
|
||||
@@ -199,7 +203,12 @@ class StreamMuxer:
|
||||
# Fragment durations may exceed the 15% allowed variance but it seems ok
|
||||
**(
|
||||
{
|
||||
"movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov",
|
||||
"movflags": (
|
||||
"empty_moov+default_base_moof"
|
||||
"+frag_discont"
|
||||
"+negative_cts_offsets"
|
||||
"+skip_trailer+delay_moov"
|
||||
),
|
||||
# Create a fragment every TARGET_PART_DURATION. The data from
|
||||
# each fragment is stored in a "Part" that can be combined with
|
||||
# the data from all the other "Part"s, plus an init section,
|
||||
@@ -662,7 +671,8 @@ def stream_worker(
|
||||
except av.FFmpegError as ex:
|
||||
container.close()
|
||||
raise StreamWorkerError(
|
||||
f"Error demuxing stream while finding first packet ({redact_av_error_string(ex)})"
|
||||
"Error demuxing stream while finding first packet"
|
||||
f" ({redact_av_error_string(ex)})"
|
||||
) from ex
|
||||
|
||||
muxer = StreamMuxer(
|
||||
|
||||
@@ -83,7 +83,8 @@ async def _refresh_subaru_data(
|
||||
for vehicle in vehicle_info.values():
|
||||
vin = vehicle[VEHICLE_VIN]
|
||||
|
||||
# Optionally send an "update" remote command to vehicle (throttled with update_interval)
|
||||
# Optionally send an "update" remote command to
|
||||
# vehicle (throttled with update_interval)
|
||||
if config_entry.options.get(CONF_UPDATE_ENABLED, False):
|
||||
await _update_subaru(vehicle, controller)
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ async def async_setup_entry(
|
||||
class SubaruLock(LockEntity):
|
||||
"""Representation of a Subaru door lock.
|
||||
|
||||
Note that the Subaru API currently does not support returning the status of the locks. Lock status is always unknown.
|
||||
Note that the Subaru API currently does not support
|
||||
returning the status of the locks. Lock status is
|
||||
always unknown.
|
||||
"""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@@ -145,7 +145,8 @@ async def async_migrate_entry(
|
||||
new_unique_id=f"{new_unique_id}_departure",
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Faulty entity with unique_id 'None_departure' migrated to new unique_id '%s'",
|
||||
"Faulty entity with unique_id 'None_departure'"
|
||||
" migrated to new unique_id '%s'",
|
||||
f"{new_unique_id}_departure",
|
||||
)
|
||||
|
||||
@@ -155,7 +156,9 @@ async def async_migrate_entry(
|
||||
)
|
||||
|
||||
if config_entry.version < 3:
|
||||
# Via stations and time/offset settings now available, which are not backwards compatible if used, changes unique id
|
||||
# Via stations and time/offset settings now available,
|
||||
# which are not backwards compatible if used,
|
||||
# changes unique id
|
||||
hass.config_entries.async_update_entry(config_entry, version=3, minor_version=1)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -73,7 +73,10 @@ class SwitchBeeDeviceEntity[_DeviceTypeT: SwitchBeeBaseDevice](
|
||||
return self._is_online and super().available
|
||||
|
||||
def _check_if_became_offline(self) -> None:
|
||||
"""Check if the device was online (now offline), log message and mark it as Unavailable."""
|
||||
"""Check if the device was online (now offline).
|
||||
|
||||
Log message and mark it as unavailable.
|
||||
"""
|
||||
|
||||
if self._is_online:
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -77,8 +77,9 @@ class SwitchBeeSwitchEntity[
|
||||
|
||||
self._check_if_became_online()
|
||||
|
||||
# timed power switch state is an integer representing the number of minutes left until it goes off
|
||||
# regulare switches state is ON/OFF (1/0 respectively)
|
||||
# timed power switch state is an integer representing
|
||||
# the number of minutes left until it goes off;
|
||||
# regular switches state is ON/OFF (1/0 respectively)
|
||||
self._attr_is_on = coordinator_device.state != ApiStateCommand.OFF
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -198,16 +198,22 @@ CLASS_BY_DEVICE = {
|
||||
SupportedModels.AIR_PURIFIER_US.value: switchbot.SwitchbotAirPurifier,
|
||||
SupportedModels.AIR_PURIFIER_TABLE_JP.value: switchbot.SwitchbotAirPurifier,
|
||||
SupportedModels.AIR_PURIFIER_TABLE_US.value: switchbot.SwitchbotAirPurifier,
|
||||
SupportedModels.EVAPORATIVE_HUMIDIFIER.value: switchbot.SwitchbotEvaporativeHumidifier,
|
||||
SupportedModels.EVAPORATIVE_HUMIDIFIER.value: (
|
||||
switchbot.SwitchbotEvaporativeHumidifier
|
||||
),
|
||||
SupportedModels.FLOOR_LAMP.value: switchbot.SwitchbotStripLight3,
|
||||
SupportedModels.STRIP_LIGHT_3.value: switchbot.SwitchbotStripLight3,
|
||||
SupportedModels.RGBICWW_FLOOR_LAMP.value: switchbot.SwitchbotRgbicLight,
|
||||
SupportedModels.RGBICWW_STRIP_LIGHT.value: switchbot.SwitchbotRgbicLight,
|
||||
SupportedModels.PERMANENT_OUTDOOR_LIGHT.value: switchbot.SwitchbotPermanentOutdoorLight,
|
||||
SupportedModels.PERMANENT_OUTDOOR_LIGHT.value: (
|
||||
switchbot.SwitchbotPermanentOutdoorLight
|
||||
),
|
||||
SupportedModels.PLUG_MINI_EU.value: switchbot.SwitchbotRelaySwitch,
|
||||
SupportedModels.RELAY_SWITCH_2PM.value: switchbot.SwitchbotRelaySwitch2PM,
|
||||
SupportedModels.GARAGE_DOOR_OPENER.value: switchbot.SwitchbotGarageDoorOpener,
|
||||
SupportedModels.SMART_THERMOSTAT_RADIATOR.value: switchbot.SwitchbotSmartThermostatRadiator,
|
||||
SupportedModels.SMART_THERMOSTAT_RADIATOR.value: (
|
||||
switchbot.SwitchbotSmartThermostatRadiator
|
||||
),
|
||||
SupportedModels.ART_FRAME.value: switchbot.SwitchbotArtFrame,
|
||||
SupportedModels.KEYPAD_VISION.value: switchbot.SwitchbotKeypadVision,
|
||||
SupportedModels.KEYPAD_VISION_PRO.value: switchbot.SwitchbotKeypadVision,
|
||||
|
||||
@@ -92,7 +92,7 @@ class SwitchBotArtFramePrevButton(SwitchBotArtFrameButtonBase):
|
||||
|
||||
|
||||
class SwitchBotMeterProCO2SyncDateTimeButton(SwitchbotEntity, ButtonEntity):
|
||||
"""Button to sync date and time on Meter Pro CO2 to the current HA instance datetime."""
|
||||
"""Button to sync date and time on Meter Pro CO2."""
|
||||
|
||||
_device: switchbot.SwitchbotMeterProCO2
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
@@ -119,7 +119,8 @@ class SwitchBotMeterProCO2SyncDateTimeButton(SwitchbotEntity, ButtonEntity):
|
||||
timestamp = int(now.timestamp())
|
||||
|
||||
_LOGGER.debug(
|
||||
"Syncing time for %s: timestamp=%s, utc_offset_hours=%s, utc_offset_minutes=%s",
|
||||
"Syncing time for %s: timestamp=%s,"
|
||||
" utc_offset_hours=%s, utc_offset_minutes=%s",
|
||||
self._address,
|
||||
timestamp,
|
||||
utc_offset_hours,
|
||||
|
||||
@@ -182,7 +182,9 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
|
||||
SwitchbotModel.PLUG_MINI_EU: switchbot.SwitchbotRelaySwitch,
|
||||
SwitchbotModel.RELAY_SWITCH_2PM: switchbot.SwitchbotRelaySwitch2PM,
|
||||
SwitchbotModel.GARAGE_DOOR_OPENER: switchbot.SwitchbotRelaySwitch,
|
||||
SwitchbotModel.SMART_THERMOSTAT_RADIATOR: switchbot.SwitchbotSmartThermostatRadiator,
|
||||
SwitchbotModel.SMART_THERMOSTAT_RADIATOR: (
|
||||
switchbot.SwitchbotSmartThermostatRadiator
|
||||
),
|
||||
SwitchbotModel.ART_FRAME: switchbot.SwitchbotArtFrame,
|
||||
SwitchbotModel.KEYPAD_VISION: switchbot.SwitchbotKeypadVision,
|
||||
SwitchbotModel.KEYPAD_VISION_PRO: switchbot.SwitchbotKeypadVision,
|
||||
|
||||
@@ -41,7 +41,8 @@ class SwitchbotEntity(
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_BLUETOOTH, self._address)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=coordinator.model, # Sometimes the modelName is missing from the advertisement data
|
||||
# Sometimes the modelName is missing from ads
|
||||
model=coordinator.model,
|
||||
name=coordinator.device_name,
|
||||
)
|
||||
self._channel: int | None = None
|
||||
|
||||
@@ -46,7 +46,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitcherConfigEntry) ->
|
||||
|
||||
# New device - create device
|
||||
_LOGGER.info(
|
||||
"Discovered Switcher device - id: %s, key: %s, name: %s, type: %s (%s), is_token_needed: %s",
|
||||
"Discovered Switcher device - id: %s, key: %s,"
|
||||
" name: %s, type: %s (%s), is_token_needed: %s",
|
||||
device.device_id,
|
||||
device.device_key,
|
||||
device.name,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user