mirror of
https://github.com/home-assistant/core.git
synced 2026-07-10 08:15:14 +01:00
Fix line length violations in components f-g (#170542)
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:
@@ -29,7 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: FastdotcomConfigEntry) -
|
||||
else:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Don't start a speedtest during startup, this will slow down the overall startup dramatically
|
||||
# Don't start a speedtest during startup, this will slow
|
||||
# down the overall startup dramatically
|
||||
async_at_started(hass, _async_finish_startup)
|
||||
return True
|
||||
|
||||
|
||||
@@ -185,7 +185,8 @@ class FeedReaderCoordinator(
|
||||
firstrun = True
|
||||
# Set last entry timestamp as epoch time if not available
|
||||
self._last_entry_timestamp = dt_util.utc_from_timestamp(0).timetuple()
|
||||
# locally cache self._last_entry_timestamp so that entries published at identical times can be processed
|
||||
# locally cache self._last_entry_timestamp so that
|
||||
# entries published at identical times can be processed
|
||||
last_entry_timestamp = self._last_entry_timestamp
|
||||
for entry in self._feed.entries:
|
||||
if firstrun or (
|
||||
|
||||
@@ -67,8 +67,9 @@ class FeedReaderEvent(CoordinatorEntity[FeedReaderCoordinator], EventEntity):
|
||||
if (data := self.coordinator.data) is None or not data:
|
||||
return
|
||||
|
||||
# RSS feeds are normally sorted reverse chronologically by published date
|
||||
# so we always take the first entry in list, since we only care about the latest entry
|
||||
# RSS feeds are normally sorted reverse chronologically
|
||||
# by published date so we always take the first entry
|
||||
# in list, since we only care about the latest entry
|
||||
feed_data: FeedParserDict = data[0]
|
||||
|
||||
if description := feed_data.get("description"):
|
||||
|
||||
@@ -74,7 +74,8 @@ class FileUploadData:
|
||||
"""Create temporary directory."""
|
||||
temp_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME
|
||||
|
||||
# If it exists, it's an old one and Home Assistant didn't shut down correctly.
|
||||
# If it exists, it's an old one and Home Assistant
|
||||
# didn't shut down correctly.
|
||||
if temp_dir.exists():
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: FingConfigEntry)
|
||||
|
||||
if coordinator.data.network_id is None:
|
||||
_LOGGER.warning(
|
||||
"Skip setting up Fing integration; Received an empty NetworkId from the request - Check if the API version is the latest"
|
||||
"Skip setting up Fing integration; Received an empty"
|
||||
" NetworkId from the request - Check if the API"
|
||||
" version is the latest"
|
||||
)
|
||||
raise ConfigEntryError(
|
||||
"The Agent's API version is outdated. Please update the agent to the latest version."
|
||||
"The Agent's API version is outdated."
|
||||
" Please update the agent to the latest version."
|
||||
)
|
||||
|
||||
config_entry.runtime_data = coordinator
|
||||
|
||||
@@ -48,7 +48,9 @@ class FingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
devices_response = await fing_api.get_devices()
|
||||
|
||||
with suppress(httpx.ConnectError):
|
||||
# The suppression is needed because the get_agent_info method isn't available for desktop agents
|
||||
# The suppression is needed because the
|
||||
# get_agent_info method isn't available
|
||||
# for desktop agents
|
||||
agent_info_response = await fing_api.get_agent_info()
|
||||
|
||||
except httpx.NetworkError as _:
|
||||
@@ -57,7 +59,8 @@ class FingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "timeout_connect"
|
||||
except httpx.HTTPStatusError as exception:
|
||||
description_placeholders["message"] = (
|
||||
f"{exception.response.status_code} - {exception.response.reason_phrase}"
|
||||
f"{exception.response.status_code}"
|
||||
f" - {exception.response.reason_phrase}"
|
||||
)
|
||||
if exception.response.status_code == 401:
|
||||
errors["base"] = "invalid_api_key"
|
||||
|
||||
@@ -69,7 +69,8 @@ class FingDataUpdateCoordinator(DataUpdateCoordinator[FingDataObject]):
|
||||
if err.response.status_code == 401:
|
||||
raise UpdateFailed("Invalid API key") from err
|
||||
raise UpdateFailed(
|
||||
f"Http request failed -> {err.response.status_code} - {err.response.reason_phrase}"
|
||||
f"Http request failed -> {err.response.status_code}"
|
||||
f" - {err.response.reason_phrase}"
|
||||
) from err
|
||||
except httpx.InvalidURL as err:
|
||||
raise UpdateFailed("Invalid hostname or IP address") from err
|
||||
|
||||
@@ -117,7 +117,10 @@ class FitbitApi(ABC):
|
||||
return devices
|
||||
|
||||
async def async_get_latest_time_series(self, resource_type: str) -> dict[str, Any]:
|
||||
"""Return the most recent value from the time series for the specified resource type."""
|
||||
"""Return the most recent value from the time series.
|
||||
|
||||
Returns the value for the specified resource type.
|
||||
"""
|
||||
client = await self._async_get_client()
|
||||
|
||||
# Set request header based on the configured unit system
|
||||
|
||||
@@ -52,7 +52,10 @@ class OAuth2FlowHandler(
|
||||
async def async_step_creation(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Create config entry from external data with Fitbit specific error handling."""
|
||||
"""Create config entry from external data.
|
||||
|
||||
Handles Fitbit specific errors.
|
||||
"""
|
||||
try:
|
||||
return await super().async_step_creation()
|
||||
except FitbitAuthException as err:
|
||||
|
||||
@@ -50,7 +50,8 @@ class FitbitUnitSystem(StrEnum):
|
||||
This is used as a header to tell the Fitbit API which type of units to return.
|
||||
https://dev.fitbit.com/build/reference/web-api/developer-guide/application-design/#Units
|
||||
|
||||
Prefer to leave unset for newer configurations to use the Home Assistant default units.
|
||||
Prefer to leave unset for newer configurations to use
|
||||
the Home Assistant default units.
|
||||
"""
|
||||
|
||||
LEGACY_DEFAULT = "default"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Exceptions for fitbit API calls.
|
||||
|
||||
These exceptions exist to provide common exceptions for the async and sync client libraries.
|
||||
These exceptions provide common exceptions for the async
|
||||
and sync client libraries.
|
||||
"""
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
@@ -115,7 +115,11 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
await self.device.set_air_temp_setpoint_away(temperature)
|
||||
else:
|
||||
await self.device.set_air_temp_setpoint_home(temperature)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
except (
|
||||
asyncio.exceptions.TimeoutError,
|
||||
ConnectionError,
|
||||
DecodingError,
|
||||
) as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_temperature",
|
||||
@@ -149,7 +153,11 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
# Set the desired ventilation mode
|
||||
ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode]
|
||||
await self.device.set_ventilation_mode(ventilation_mode)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
except (
|
||||
asyncio.exceptions.TimeoutError,
|
||||
ConnectionError,
|
||||
DecodingError,
|
||||
) as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_preset_mode",
|
||||
@@ -175,7 +183,11 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
await self.device.set_ventilation_mode(VENTILATION_MODE_STOP)
|
||||
else:
|
||||
await self.device.set_ventilation_mode(VENTILATION_MODE_HOME)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
except (
|
||||
asyncio.exceptions.TimeoutError,
|
||||
ConnectionError,
|
||||
DecodingError,
|
||||
) as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_hvac_mode",
|
||||
|
||||
@@ -39,7 +39,8 @@ class FlexitNumberEntityDescription(NumberEntityDescription):
|
||||
set_native_value_fn: Callable[[FlexitBACnet], Callable[[int], Awaitable[None]]]
|
||||
|
||||
|
||||
# Setpoints for Away, Home and High are dependent of each other. Fireplace and Cooker Hood
|
||||
# Setpoints for Away, Home and High are dependent of each
|
||||
# other. Fireplace and Cooker Hood
|
||||
# have setpoints between 0 (MIN_FAN_SETPOINT) and 100 (MAX_FAN_SETPOINT).
|
||||
# See the table below for all the setpoints.
|
||||
#
|
||||
|
||||
@@ -190,7 +190,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: FluxLedConfigEntry) -> b
|
||||
if entry.unique_id and discovery.get(ATTR_ID):
|
||||
mac = dr.format_mac(cast(str, discovery[ATTR_ID]))
|
||||
if not mac_matches_by_one(mac, entry.unique_id):
|
||||
# The device is offline and another flux_led device is now using the ip address
|
||||
# The device is offline and another flux_led device
|
||||
# is now using the ip address
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unexpected device found at {host}; Expected {entry.unique_id}, found"
|
||||
f" {mac}"
|
||||
|
||||
@@ -105,7 +105,11 @@ class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_AZIMUTH: user_input[CONF_AZIMUTH],
|
||||
CONF_MODULES_POWER: user_input[CONF_MODULES_POWER],
|
||||
},
|
||||
"title": f"{user_input[CONF_DECLINATION]}° / {user_input[CONF_AZIMUTH]}° / {user_input[CONF_MODULES_POWER]}W",
|
||||
"title": (
|
||||
f"{user_input[CONF_DECLINATION]}°"
|
||||
f" / {user_input[CONF_AZIMUTH]}°"
|
||||
f" / {user_input[CONF_MODULES_POWER]}W"
|
||||
),
|
||||
"unique_id": None,
|
||||
},
|
||||
],
|
||||
@@ -240,7 +244,11 @@ class PlaneSubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=f"{user_input[CONF_DECLINATION]}° / {user_input[CONF_AZIMUTH]}° / {user_input[CONF_MODULES_POWER]}W",
|
||||
title=(
|
||||
f"{user_input[CONF_DECLINATION]}°"
|
||||
f" / {user_input[CONF_AZIMUTH]}°"
|
||||
f" / {user_input[CONF_MODULES_POWER]}W"
|
||||
),
|
||||
data={
|
||||
CONF_DECLINATION: user_input[CONF_DECLINATION],
|
||||
CONF_AZIMUTH: user_input[CONF_AZIMUTH],
|
||||
@@ -276,7 +284,11 @@ class PlaneSubentryFlowHandler(ConfigSubentryFlow):
|
||||
CONF_AZIMUTH: user_input[CONF_AZIMUTH],
|
||||
CONF_MODULES_POWER: user_input[CONF_MODULES_POWER],
|
||||
},
|
||||
title=f"{user_input[CONF_DECLINATION]}° / {user_input[CONF_AZIMUTH]}° / {user_input[CONF_MODULES_POWER]}W",
|
||||
title=(
|
||||
f"{user_input[CONF_DECLINATION]}°"
|
||||
f" / {user_input[CONF_AZIMUTH]}°"
|
||||
f" / {user_input[CONF_MODULES_POWER]}W"
|
||||
),
|
||||
):
|
||||
if not entry.update_listeners:
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
@@ -36,7 +36,9 @@ async def async_get_config_entry_diagnostics(
|
||||
},
|
||||
"data": {
|
||||
"energy_production_today": coordinator.data.energy_production_today,
|
||||
"energy_production_today_remaining": coordinator.data.energy_production_today_remaining,
|
||||
"energy_production_today_remaining": (
|
||||
coordinator.data.energy_production_today_remaining
|
||||
),
|
||||
"energy_production_tomorrow": coordinator.data.energy_production_tomorrow,
|
||||
"energy_current_hour": coordinator.data.energy_current_hour,
|
||||
"power_production_now": coordinator.data.power_production_now,
|
||||
|
||||
@@ -58,11 +58,16 @@ OWNTONE_TYPE_TO_MEDIA_TYPE = {
|
||||
}
|
||||
MEDIA_TYPE_TO_OWNTONE_TYPE = {v: k for k, v in OWNTONE_TYPE_TO_MEDIA_TYPE.items()}
|
||||
|
||||
# media_content_id is a uri in the form of SCHEMA:Title:OwnToneURI:Subtype (Subtype only used for Genre)
|
||||
# OwnToneURI is in format library:type:id (for directories, id is path)
|
||||
# media_content_type - type of item (mostly used to check if playable or can expand)
|
||||
# OwnTone type may differ from media_content_type when media_content_type is a directory
|
||||
# OwnTone type is used in our own branching, but media_content_type is used for determining playability
|
||||
# media_content_id is a uri in the form of
|
||||
# SCHEMA:Title:OwnToneURI:Subtype (Subtype only for Genre)
|
||||
# OwnToneURI is in format library:type:id
|
||||
# (for directories, id is path)
|
||||
# media_content_type - type of item
|
||||
# (mostly used to check if playable or can expand)
|
||||
# OwnTone type may differ from media_content_type
|
||||
# when media_content_type is a directory
|
||||
# OwnTone type is used in our own branching, but
|
||||
# media_content_type is used for determining playability
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -348,7 +348,7 @@ class ForkedDaapdMaster(MediaPlayerEntity):
|
||||
self._queue["count"] >= 1
|
||||
and self._queue["items"][0]["data_kind"] == "pipe"
|
||||
and self._queue["items"][0]["title"] in KNOWN_PIPES
|
||||
): # if we're playing a pipe, set the source automatically so we can forward controls
|
||||
): # if playing a pipe, set source to forward controls
|
||||
self._source = f"{self._queue['items'][0]['title']} (pipe)"
|
||||
self._update_track_info()
|
||||
event.set()
|
||||
|
||||
@@ -17,7 +17,7 @@ type FoscamConfigEntry = ConfigEntry[FoscamCoordinator]
|
||||
|
||||
@dataclass
|
||||
class FoscamDeviceInfo:
|
||||
"""A data class representing the current state and configuration of a Foscam camera device."""
|
||||
"""Represent the current state and config of a Foscam camera."""
|
||||
|
||||
dev_info: dict
|
||||
product_info: dict
|
||||
|
||||
@@ -15,14 +15,22 @@ from .entity import FoscamEntity
|
||||
|
||||
|
||||
def handle_ir_turn_on(session: FoscamCamera) -> None:
|
||||
"""Turn on IR LED: sets IR mode to auto (if supported), then turns off the IR LED."""
|
||||
"""Turn on IR LED.
|
||||
|
||||
Sets IR mode to auto (if supported), then turns off
|
||||
the IR LED.
|
||||
"""
|
||||
|
||||
session.set_infra_led_config(1)
|
||||
session.open_infra_led()
|
||||
|
||||
|
||||
def handle_ir_turn_off(session: FoscamCamera) -> None:
|
||||
"""Turn off IR LED: sets IR mode to manual (if supported), then turns open the IR LED."""
|
||||
"""Turn off IR LED.
|
||||
|
||||
Sets IR mode to manual (if supported), then turns
|
||||
open the IR LED.
|
||||
"""
|
||||
|
||||
session.set_infra_led_config(0)
|
||||
session.close_infra_led()
|
||||
|
||||
@@ -112,7 +112,11 @@ class FreeboxDwsSensor(FreeboxHomeBinarySensor):
|
||||
|
||||
|
||||
class FreeboxCoverSensor(FreeboxHomeBinarySensor):
|
||||
"""Representation of a cover Freebox plastic removal cover binary sensor (for some sensors: motion detector, door opener detector...)."""
|
||||
"""Represent a Freebox plastic removal cover sensor.
|
||||
|
||||
Applies to some sensors: motion detector,
|
||||
door opener detector, etc.
|
||||
"""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.SAFETY
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
@@ -143,7 +143,8 @@ class FreeboxRouter:
|
||||
|
||||
fbx_devices: list[dict[str, Any]] = []
|
||||
|
||||
# Access to Host list not available in bridge mode, API return error_code 'nodev'
|
||||
# Access to Host list not available in bridge mode,
|
||||
# API return error_code 'nodev'
|
||||
if self.supports_hosts:
|
||||
self.supports_hosts, fbx_devices = await get_hosts_list_if_supported(
|
||||
self._api
|
||||
@@ -180,7 +181,8 @@ class FreeboxRouter:
|
||||
# System sensors
|
||||
syst_datas: dict[str, Any] = await self._api.system.get_config()
|
||||
|
||||
# According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree.
|
||||
# According to the doc `syst_datas["sensors"]` is
|
||||
# temperature sensors in celsius degree.
|
||||
# Name and id of sensors may vary under Freebox devices.
|
||||
for sensor in syst_datas["sensors"]:
|
||||
self.sensors_temperature[sensor["name"]] = sensor.get("value")
|
||||
|
||||
@@ -38,7 +38,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def _get_valid_tracker(hass: HomeAssistant, device: Device) -> Tracker | None:
|
||||
"""Test if the tracker returns valid data and return it.
|
||||
|
||||
Malformed data might indicate the tracker is broken or hasn't been properly registered with the app.
|
||||
Malformed data might indicate the tracker is broken or
|
||||
hasn't been properly registered with the app.
|
||||
"""
|
||||
client = ApiClient(
|
||||
serial_number=device.serialnumber,
|
||||
|
||||
@@ -188,15 +188,21 @@ class FritzButton(ButtonEntity):
|
||||
"""Triggers Fritz!Box service."""
|
||||
if self.entity_description.key == "cleanup":
|
||||
_LOGGER.warning(
|
||||
"The 'cleanup' button is deprecated and will be removed in Home Assistant Core 2026.11.0. "
|
||||
"Please update your automations and dashboards to remove any usage of this button. "
|
||||
"The action is now performed automatically at each data refresh",
|
||||
"The 'cleanup' button is deprecated and will"
|
||||
" be removed in Home Assistant Core"
|
||||
" 2026.11.0. Please update your automations"
|
||||
" and dashboards to remove any usage of"
|
||||
" this button. The action is now performed"
|
||||
" automatically at each data refresh",
|
||||
)
|
||||
elif self.entity_description.key == "firmware_update":
|
||||
_LOGGER.warning(
|
||||
"The 'firmware update' button is deprecated and will be removed in Home Assistant Core "
|
||||
"2026.11.0. It has been superseded by an update entity. Please update your automations "
|
||||
"and dashboards to remove any usage of this button",
|
||||
"The 'firmware update' button is deprecated"
|
||||
" and will be removed in Home Assistant"
|
||||
" Core 2026.11.0. It has been superseded"
|
||||
" by an update entity. Please update your"
|
||||
" automations and dashboards to remove"
|
||||
" any usage of this button",
|
||||
)
|
||||
await self.entity_description.press_action(self.avm_wrapper)
|
||||
|
||||
|
||||
@@ -376,8 +376,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if (port == DEFAULT_HTTP_PORT and not ssl) or (
|
||||
port == DEFAULT_HTTPS_PORT and ssl
|
||||
):
|
||||
# don't show default ports in reconfigure flow, as they are determined by ssl value
|
||||
# this allows the user to toggle ssl without having to change the port
|
||||
# don't show default ports in reconfigure flow,
|
||||
# as they are determined by ssl value
|
||||
# this allows the user to toggle ssl
|
||||
# without having to change the port
|
||||
port = vol.UNDEFINED
|
||||
|
||||
return self._show_setup_form_reconfigure(
|
||||
|
||||
@@ -49,8 +49,9 @@ def _wifi_naming(
|
||||
"""Return a friendly name for a Wi-Fi network."""
|
||||
|
||||
if wifi_index == 2 and wifi_count == 4:
|
||||
# In case of 4 Wi-Fi networks, the 2nd one is used for internal communication
|
||||
# between mesh devices and should not be named like the others to avoid confusion
|
||||
# In case of 4 Wi-Fi networks, the 2nd one is used
|
||||
# for internal communication between mesh devices and
|
||||
# should not be named like the others to avoid confusion
|
||||
return None
|
||||
|
||||
if (wifi_index + 1) == wifi_count:
|
||||
@@ -110,7 +111,10 @@ async def _migrate_to_new_unique_id(
|
||||
description += f" ({WIFI_STANDARD[index]})"
|
||||
|
||||
old_unique_id = f"{avm_wrapper.unique_id}-{slugify(description)}"
|
||||
new_unique_id = f"{avm_wrapper.unique_id}-wi_fi_{slugify(_wifi_naming(network, index - 1, len(networks)))}"
|
||||
new_unique_id = (
|
||||
f"{avm_wrapper.unique_id}-wi_fi_"
|
||||
f"{slugify(_wifi_naming(network, index - 1, len(networks)))}"
|
||||
)
|
||||
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.SWITCH, DOMAIN, old_unique_id
|
||||
@@ -454,7 +458,9 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
|
||||
|
||||
self._attributes = {}
|
||||
self.connection_type = connection_type
|
||||
self.port_mapping = port_mapping # dict in the format as it comes from fritzconnection. eg: {'NewRemoteHost': '0.0.0.0', 'NewExternalPort': 22, 'NewProtocol': 'TCP', 'NewInternalPort': 22, 'NewInternalClient': '192.168.178.31', 'NewEnabled': True, 'NewPortMappingDescription': 'Beast SSH ', 'NewLeaseDuration': 0}
|
||||
# dict in the format as it comes from fritzconnection,
|
||||
# eg: {"NewRemoteHost": "0.0.0.0", "NewExternalPort": 22, ...}
|
||||
self.port_mapping = port_mapping
|
||||
self._idx = idx # needed for update routine
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class FritzBoxEntity(CoordinatorEntity[FritzboxDataUpdateCoordinator], ABC):
|
||||
|
||||
|
||||
class FritzBoxDeviceEntity(FritzBoxEntity):
|
||||
"""Reflects FritzhomeDevice and uses its attributes to construct FritzBoxDeviceEntity."""
|
||||
"""Reflect FritzhomeDevice and construct FritzBoxDeviceEntity."""
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -138,7 +138,8 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
|
||||
)
|
||||
else:
|
||||
LOGGER.debug(
|
||||
"device has no fullcolorsupport, using supported colors with 'setcolor'"
|
||||
"device has no fullcolorsupport,"
|
||||
" using supported colors with 'setcolor'"
|
||||
)
|
||||
# find supported hs values closest to what user selected
|
||||
hue = min(
|
||||
|
||||
@@ -71,7 +71,7 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
def check_lock_state(self) -> None:
|
||||
"""Raise an Error if manual switching via FRITZ!Box user interface is disabled."""
|
||||
"""Raise an Error if manual switching via FRITZ!Box is disabled."""
|
||||
if self.data.lock:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -62,7 +62,7 @@ class FritzBoxPhonebook:
|
||||
self.prefixes = prefixes
|
||||
|
||||
def init_phonebook(self) -> None:
|
||||
"""Establish a connection to the FRITZ!Box and check if phonebook_id is valid."""
|
||||
"""Connect to the FRITZ!Box and check if phonebook_id is valid."""
|
||||
self.fph = FritzPhonebook(
|
||||
address=self.host,
|
||||
user=self.username,
|
||||
|
||||
@@ -84,8 +84,9 @@ class FroniusSolarNet:
|
||||
self.coordinator_lock = asyncio.Lock()
|
||||
self.fronius = fronius
|
||||
self.host: str = entry.data[CONF_HOST]
|
||||
# entry.unique_id is either logger uid or first inverter uid if no logger available
|
||||
# prepended by "solar_net_" to have individual device for whole system (power_flow)
|
||||
# entry.unique_id is either logger uid or first inverter
|
||||
# uid if no logger available prepended by "solar_net_"
|
||||
# to have individual device for whole system (power_flow)
|
||||
self.solar_net_device_id = f"solar_net_{entry.unique_id}"
|
||||
self.system_device_info: DeviceInfo | None = None
|
||||
|
||||
@@ -217,7 +218,8 @@ class FroniusSolarNet:
|
||||
await _coordinator.async_config_entry_first_refresh()
|
||||
self.inverter_coordinators.append(_coordinator)
|
||||
|
||||
# Only for re-scans. Initial setup adds entities through sensor.async_setup_entry
|
||||
# Only for re-scans. Initial setup adds entities
|
||||
# through sensor.async_setup_entry
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
|
||||
|
||||
|
||||
@@ -772,7 +772,10 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
|
||||
return self.coordinator.data[self.solar_net_id]
|
||||
|
||||
def _get_entity_value(self) -> Any:
|
||||
"""Extract entity value from coordinator. Raises KeyError if not included in latest update."""
|
||||
"""Extract entity value from coordinator.
|
||||
|
||||
Raises KeyError if not included in latest update.
|
||||
"""
|
||||
new_value = self.coordinator.data[self.solar_net_id][self.response_key]["value"]
|
||||
if new_value is None:
|
||||
return self.entity_description.default_value
|
||||
@@ -790,8 +793,10 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
|
||||
try:
|
||||
self._attr_native_value = self._get_entity_value()
|
||||
except KeyError:
|
||||
# sets state to `None` if no default_value is defined in entity description
|
||||
# KeyError: raised when omitted in response - eg. at night when no production
|
||||
# sets state to `None` if no default_value is defined
|
||||
# in entity description
|
||||
# KeyError: raised when omitted in response
|
||||
# eg. at night when no production
|
||||
self._attr_native_value = self.entity_description.default_value
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -509,7 +509,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
_LOGGER.info("Using frontend from PR #%s", dev_pr_number)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to download PR #%s: %s, falling back to the integrated frontend",
|
||||
"Failed to download PR #%s: %s, falling back"
|
||||
" to the integrated frontend",
|
||||
dev_pr_number,
|
||||
err,
|
||||
)
|
||||
|
||||
@@ -129,7 +129,8 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def _async_step_device_config_if_needed(self) -> ConfigFlowResult:
|
||||
"""Most users will not have changed the default PIN on their radio.
|
||||
|
||||
We try to use this default PIN, and only if this fails ask for it via `async_step_device_config`
|
||||
We try to use this default PIN, and only if this fails
|
||||
ask for it via `async_step_device_config`
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -155,7 +156,10 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Allow the user to confirm adding the device. Used when the default PIN could successfully be used."""
|
||||
"""Allow the user to confirm adding the device.
|
||||
|
||||
Used when the default PIN could successfully be used.
|
||||
"""
|
||||
|
||||
if user_input is not None:
|
||||
return await self._async_create_entry()
|
||||
|
||||
@@ -445,7 +445,8 @@ class AFSAPIDevice(MediaPlayerEntity):
|
||||
if len(keys) != 1:
|
||||
raise BrowseError("Presets can only have 1 level")
|
||||
|
||||
# Keys of presets are 0-based, while the list shown on the device starts from 1
|
||||
# Keys of presets are 0-based, while the list shown
|
||||
# on the device starts from 1
|
||||
preset = int(keys[0]) - 1
|
||||
|
||||
await self.fs_device.select_preset(preset)
|
||||
|
||||
@@ -29,7 +29,10 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class FGLairOutsideTemperature(FGLairEntity, SensorEntity):
|
||||
"""Entity representing outside temperature sensed by the outside unit of a Fujitsu Heatpump."""
|
||||
"""Entity representing outside temperature.
|
||||
|
||||
Sensed by the outside unit of a Fujitsu Heatpump.
|
||||
"""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
||||
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
|
||||
@@ -21,7 +21,9 @@ def valid_global_mac_address(mac: str | None) -> bool:
|
||||
return False
|
||||
try:
|
||||
first_octet = int(mac.split(":")[0], 16)
|
||||
# If the second least-significant bit is set, it's a locally administered address, should not be used as an ID
|
||||
# If the second least-significant bit is set, it's a
|
||||
# locally administered address, should not be used as
|
||||
# an ID
|
||||
return not bool(first_octet & 0x2)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@@ -66,7 +66,8 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
|
||||
) from err
|
||||
_LOGGER.debug("Data successfully updated")
|
||||
|
||||
# data must be assigned before _async_add_remove_devices, as it is uses to set-up possible new devices
|
||||
# data must be assigned before _async_add_remove_devices,
|
||||
# as it is uses to set-up possible new devices
|
||||
self.data = data
|
||||
self._async_add_remove_devices()
|
||||
|
||||
|
||||
@@ -252,7 +252,10 @@ class GaradgetCover(CoverEntity):
|
||||
|
||||
def _get_variable(self, var):
|
||||
"""Get latest status."""
|
||||
url = f"{self.particle_url}/v1/devices/{self.device_id}/{var}?access_token={self.access_token}"
|
||||
url = (
|
||||
f"{self.particle_url}/v1/devices/{self.device_id}"
|
||||
f"/{var}?access_token={self.access_token}"
|
||||
)
|
||||
ret = requests.get(url, timeout=10)
|
||||
result = {}
|
||||
for pairs in ret.json()["result"].split("|"):
|
||||
|
||||
@@ -345,7 +345,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
hass = self.hass
|
||||
if user_input:
|
||||
# Secondary validation because serialised vol can't seem to handle this complexity:
|
||||
# Secondary validation because serialised vol can't
|
||||
# seem to handle this complexity:
|
||||
if not user_input.get(CONF_STILL_IMAGE_URL) and not user_input.get(
|
||||
CONF_STREAM_SOURCE
|
||||
):
|
||||
@@ -429,7 +430,8 @@ class GenericOptionsFlowHandler(OptionsFlow):
|
||||
hass = self.hass
|
||||
|
||||
if user_input:
|
||||
# Secondary validation because serialised vol can't seem to handle this complexity:
|
||||
# Secondary validation because serialised vol can't
|
||||
# seem to handle this complexity:
|
||||
if not user_input.get(CONF_STILL_IMAGE_URL) and not user_input.get(
|
||||
CONF_STREAM_SOURCE
|
||||
):
|
||||
@@ -580,7 +582,10 @@ async def ws_start_preview(
|
||||
ha_stream_url = None
|
||||
|
||||
if user_input.get(CONF_STILL_IMAGE_URL):
|
||||
ha_still_url = f"/api/generic/preview_flow_image/{msg['flow_id']}?t={datetime.now().isoformat()}"
|
||||
ha_still_url = (
|
||||
"/api/generic/preview_flow_image"
|
||||
f"/{msg['flow_id']}?t={datetime.now().isoformat()}"
|
||||
)
|
||||
_LOGGER.debug("Got preview still URL: %s", ha_still_url)
|
||||
|
||||
if ha_stream := flow.preview_stream:
|
||||
|
||||
@@ -696,7 +696,8 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
|
||||
f" {self.preset_modes}"
|
||||
)
|
||||
if preset_mode == self._attr_preset_mode:
|
||||
# I don't think we need to call async_write_ha_state if we didn't change the state
|
||||
# I don't think we need to call async_write_ha_state
|
||||
# if we didn't change the state
|
||||
return
|
||||
if preset_mode == PRESET_NONE:
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
|
||||
@@ -29,8 +29,10 @@ class GeocachingCacheEntity(GeocachingBaseEntity):
|
||||
super().__init__(coordinator)
|
||||
self.cache = cache
|
||||
|
||||
# A device can have multiple entities, and for a cache which requires multiple entities we want to group them together.
|
||||
# Therefore, we create a device for each cache, which holds all related entities.
|
||||
# A device can have multiple entities, and for a cache
|
||||
# which requires multiple entities we want to group them
|
||||
# together. Therefore, we create a device for each cache,
|
||||
# which holds all related entities.
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=f"Geocache {cache.name}",
|
||||
identifiers={(DOMAIN, cast(str, cache.reference_code))},
|
||||
|
||||
@@ -46,7 +46,8 @@ class GeocachingOAuth2Implementation(AuthImplementation):
|
||||
"redirect_uri": redirect_uri,
|
||||
}
|
||||
token = await self._token_request(data)
|
||||
# Store the redirect_uri (Needed for refreshing token, but not according to oAuth2 spec!)
|
||||
# Store the redirect_uri
|
||||
# (Needed for refreshing token, but not per oAuth2 spec!)
|
||||
token["redirect_uri"] = redirect_uri
|
||||
return token
|
||||
|
||||
@@ -57,7 +58,8 @@ class GeocachingOAuth2Implementation(AuthImplementation):
|
||||
"client_secret": self.client_secret,
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": token["refresh_token"],
|
||||
# Add previously stored redirect_uri (Mandatory, but not according to oAuth2 spec!)
|
||||
# Add previously stored redirect_uri
|
||||
# (Mandatory, but not per oAuth2 spec!)
|
||||
"redirect_uri": token["redirect_uri"],
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,9 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
# Base class for a cache entity.
|
||||
# Sets the device, ID and translation settings to correctly group the entity to the correct cache device and give it the correct name.
|
||||
# Sets the device, ID and translation settings to correctly
|
||||
# group the entity to the correct cache device and give it
|
||||
# the correct name.
|
||||
class GeoEntityBaseCache(GeocachingCacheEntity, SensorEntity):
|
||||
"""Base class for cache entities."""
|
||||
|
||||
@@ -130,7 +132,8 @@ class GeoEntityBaseCache(GeocachingCacheEntity, SensorEntity):
|
||||
|
||||
self._attr_unique_id = f"{cache.reference_code}_{key}"
|
||||
|
||||
# The translation key determines the name of the entity as this is the lookup for the `strings.json` file.
|
||||
# The translation key determines the name of the entity
|
||||
# as this is the lookup for the `strings.json` file.
|
||||
self._attr_translation_key = f"cache_{key}"
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator[GiosSensors]):
|
||||
if TYPE_CHECKING:
|
||||
# Station ID is Optional in the library, but here we know it is set for sure
|
||||
# so we can safely assert it is not None for type checking purposes
|
||||
# Gios instance is created only with a valid station ID in the async_setup_entry.
|
||||
# Gios instance is created only with a valid station
|
||||
# ID in the async_setup_entry.
|
||||
assert station_id is not None
|
||||
|
||||
self.device_info = DeviceInfo(
|
||||
|
||||
@@ -143,7 +143,8 @@ class GitHubConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def _wait_for_login() -> None:
|
||||
if TYPE_CHECKING:
|
||||
# mypy is not aware that we can't get here without having these set already
|
||||
# mypy is not aware that we can't get here
|
||||
# without having these set already
|
||||
assert self._device is not None
|
||||
assert self._login_device is not None
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ _AUTH = "auth"
|
||||
|
||||
|
||||
def _validate_auth(config: dict) -> dict:
|
||||
"""Validate that username and password are only set when a URL is configured or when debug UI is enabled."""
|
||||
"""Validate username/password only when URL is configured or debug UI enabled."""
|
||||
auth_exists = CONF_USERNAME in config
|
||||
debug_ui_enabled = config.get(CONF_DEBUG_UI, False)
|
||||
|
||||
@@ -83,7 +83,8 @@ def _validate_auth(config: dict) -> dict:
|
||||
|
||||
if auth_exists and CONF_URL not in config and not debug_ui_enabled:
|
||||
raise vol.Invalid(
|
||||
"Username and password can only be set when a URL is configured or debug_ui is true"
|
||||
"Username and password can only be set when a URL is"
|
||||
" configured or debug_ui is true"
|
||||
)
|
||||
|
||||
return config
|
||||
@@ -363,7 +364,8 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||
|
||||
if camera.platform.platform_name == "generic":
|
||||
# This is a workaround to use ffmpeg for generic cameras
|
||||
# A proper fix will be added in the future together with supporting multiple streams per camera
|
||||
# A proper fix will be added in the future together
|
||||
# with supporting multiple streams per camera
|
||||
stream_source = "ffmpeg:" + stream_source
|
||||
|
||||
if not self.async_is_supported(stream_source):
|
||||
@@ -407,7 +409,8 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||
[
|
||||
stream_source,
|
||||
# We are setting any ffmpeg rtsp related logs to debug
|
||||
# Connection problems to the camera will be logged by the first stream
|
||||
# Connection problems to the camera will be
|
||||
# logged by the first stream
|
||||
# Therefore setting it to debug will not hide any important logs
|
||||
f"ffmpeg:{identifier}#audio=opus#query=log_level=debug",
|
||||
],
|
||||
|
||||
@@ -64,7 +64,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bo
|
||||
async def async_check_port(
|
||||
hass: HomeAssistant, entry: GoodweConfigEntry, host: str
|
||||
) -> Inverter:
|
||||
"""Check the communication port of the inverter, it may have changed after a firmware update."""
|
||||
"""Check the communication port of the inverter.
|
||||
|
||||
It may have changed after a firmware update.
|
||||
"""
|
||||
inverter, port = await GoodweFlowHandler.async_detect_inverter_port(host=host)
|
||||
family = type(inverter).__name__
|
||||
hass.config_entries.async_update_entry(
|
||||
|
||||
@@ -69,7 +69,8 @@ async def async_setup_entry(
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Active mode %s not found in Goodwe Inverter Operation Mode Entity. Skipping entity creation",
|
||||
"Active mode %s not found in Goodwe Inverter Operation"
|
||||
" Mode Entity. Skipping entity creation",
|
||||
active_mode,
|
||||
)
|
||||
|
||||
|
||||
@@ -48,10 +48,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||
BATTERY_SOC = "battery_soc"
|
||||
|
||||
# Sensors that are reset to 0 at midnight.
|
||||
# The inverter is only powered by the solar panels and not mains power, so it goes dead when the sun goes down.
|
||||
# The "_day" sensors are reset to 0 when the inverter wakes up in the morning when the sun comes up and power to the inverter is restored.
|
||||
# This makes sure daily values are reset at midnight instead of at sunrise.
|
||||
# When the inverter has a battery connected, HomeAssistant will not reset the values but let the inverter reset them by looking at the unavailable state of the inverter.
|
||||
# The inverter is only powered by the solar panels and not
|
||||
# mains power, so it goes dead when the sun goes down.
|
||||
# The "_day" sensors are reset to 0 when the inverter wakes
|
||||
# up in the morning when the sun comes up and power to the
|
||||
# inverter is restored.
|
||||
# This makes sure daily values are reset at midnight instead
|
||||
# of at sunrise.
|
||||
# When the inverter has a battery connected, HomeAssistant
|
||||
# will not reset the values but let the inverter reset them
|
||||
# by looking at the unavailable state of the inverter.
|
||||
DAILY_RESET = ["e_day", "e_load_day"]
|
||||
|
||||
_MAIN_SENSORS = (
|
||||
@@ -242,7 +248,8 @@ class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity):
|
||||
|
||||
Some sensors values like daily produced energy are kept available,
|
||||
even when the inverter is in sleep mode and no longer responds to request.
|
||||
In contrast to "total" sensors, these "daily" sensors need to be reset to 0 on midnight.
|
||||
In contrast to "total" sensors, these "daily" sensors
|
||||
need to be reset to 0 on midnight.
|
||||
"""
|
||||
if not self.coordinator.last_update_success:
|
||||
self.coordinator.reset_sensor(self._sensor.id_)
|
||||
|
||||
@@ -510,7 +510,8 @@ class GoogleCalendarEntity(
|
||||
def _get_calendar_event(event: Event) -> CalendarEvent:
|
||||
"""Return a CalendarEvent from an API event."""
|
||||
rrule: str | None = None
|
||||
# Home Assistant expects a single RRULE: and all other rule types are unsupported or ignored
|
||||
# Home Assistant expects a single RRULE: and all other
|
||||
# rule types are unsupported or ignored
|
||||
if (
|
||||
len(event.recurrence) == 1
|
||||
and (raw_rule := event.recurrence[0])
|
||||
|
||||
@@ -192,7 +192,8 @@ class OAuth2FlowHandler(
|
||||
primary_calendar = await calendar_service.async_get_calendar("primary")
|
||||
except ApiForbiddenException as err:
|
||||
_LOGGER.error(
|
||||
"Error reading primary calendar, make sure Google Calendar API is enabled: %s",
|
||||
"Error reading primary calendar, make sure"
|
||||
" Google Calendar API is enabled: %s",
|
||||
err,
|
||||
)
|
||||
return self.async_abort(
|
||||
|
||||
@@ -91,7 +91,8 @@ def _is_location_already_configured(
|
||||
for subentry in entry.subentries.values():
|
||||
# A more accurate way is to use the haversine formula, but for simplicity
|
||||
# we use a simple distance check. The epsilon value is small anyway.
|
||||
# This is mostly to capture cases where the user has slightly moved the location pin.
|
||||
# This is mostly to capture cases where the user
|
||||
# has slightly moved the location pin.
|
||||
if (
|
||||
abs(subentry.data[CONF_LATITUDE] - new_data[CONF_LATITUDE]) <= epsilon
|
||||
and abs(subentry.data[CONF_LONGITUDE] - new_data[CONF_LONGITUDE])
|
||||
|
||||
@@ -234,7 +234,11 @@ class AirQualitySensorEntity(
|
||||
"""Set up Air Quality Sensors."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{description.key}_{subentry.data[CONF_LATITUDE]}_{subentry.data[CONF_LONGITUDE]}"
|
||||
self._attr_unique_id = (
|
||||
f"{description.key}"
|
||||
f"_{subentry.data[CONF_LATITUDE]}"
|
||||
f"_{subentry.data[CONF_LONGITUDE]}"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, f"{self.coordinator.config_entry.entry_id}_{subentry_id}")
|
||||
|
||||
@@ -172,7 +172,7 @@ class AbstractConfig(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def get_local_webhook_id(self, agent_user_id):
|
||||
"""Return the webhook ID to be used for actions for a given agent user id via the local SDK."""
|
||||
"""Return the webhook ID for a given agent user id via the local SDK."""
|
||||
|
||||
@abstractmethod
|
||||
def get_agent_user_id_from_context(self, context):
|
||||
@@ -425,7 +425,8 @@ class AbstractConfig(ABC):
|
||||
)
|
||||
|
||||
if (agent_user_id := self.get_agent_user_id_from_webhook(webhook_id)) is None:
|
||||
# No agent user linked to this webhook, means that the user has somehow unregistered
|
||||
# No agent user linked to this webhook, means that
|
||||
# the user has somehow unregistered
|
||||
# removing webhook and stopping processing of this request.
|
||||
_LOGGER.error(
|
||||
(
|
||||
|
||||
@@ -138,7 +138,7 @@ class GoogleConfig(AbstractConfig):
|
||||
return found_agent_user_id
|
||||
|
||||
def get_local_webhook_id(self, agent_user_id):
|
||||
"""Return the webhook ID to be used for actions for a given agent user id via the local SDK."""
|
||||
"""Return the webhook ID for a given agent user id via the local SDK."""
|
||||
if data := self._store.agent_user_ids.get(agent_user_id):
|
||||
return data[STORE_GOOGLE_LOCAL_WEBHOOK_ID]
|
||||
return None
|
||||
@@ -323,7 +323,8 @@ class GoogleConfigStore:
|
||||
if (data := await self._store.async_load()) is None:
|
||||
# if the store is not found create an empty one
|
||||
# Note that the first request is always a cloud request,
|
||||
# and that will store the correct agent user id to be used for local requests
|
||||
# and that will store the correct agent user id
|
||||
# to be used for local requests
|
||||
data = {
|
||||
STORE_AGENT_USER_IDS: {},
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ def async_enable_report_state(
|
||||
{"devices": {"states": pending.popleft()}}
|
||||
)
|
||||
|
||||
# If things got queued up in last batch while we were reporting, schedule ourselves again
|
||||
# If things got queued up in last batch while we were
|
||||
# reporting, schedule ourselves again
|
||||
if pending[0]:
|
||||
unsub_pending = async_call_later(
|
||||
hass, REPORT_STATE_WINDOW, report_states_job
|
||||
@@ -111,8 +112,8 @@ def async_enable_report_state(
|
||||
result = await google_config.async_sync_notification_all(event_id, payload)
|
||||
if result != 200:
|
||||
_LOGGER.error(
|
||||
"Unable to send notification with result code: %s, check log for more"
|
||||
" info",
|
||||
"Unable to send notification with result"
|
||||
" code: %s, check log for more info",
|
||||
result,
|
||||
)
|
||||
|
||||
@@ -129,7 +130,8 @@ def async_enable_report_state(
|
||||
|
||||
_LOGGER.debug("Scheduling report state for %s: %s", changed_entity, entity_data)
|
||||
|
||||
# If a significant change is already scheduled and we have another significant one,
|
||||
# If a significant change is already scheduled and we
|
||||
# have another significant one,
|
||||
# let's create a new batch of changes
|
||||
if changed_entity in pending[-1]:
|
||||
pending.append({})
|
||||
|
||||
@@ -1637,7 +1637,9 @@ class ArmDisArmTrait(_Trait):
|
||||
AlarmControlPanelState.ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME,
|
||||
AlarmControlPanelState.ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT,
|
||||
AlarmControlPanelState.ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY,
|
||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
|
||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS: (
|
||||
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
|
||||
),
|
||||
AlarmControlPanelState.TRIGGERED: AlarmControlPanelEntityFeature.TRIGGER,
|
||||
}
|
||||
"""The list of states to support in increasing security state."""
|
||||
|
||||
@@ -137,7 +137,11 @@ def default_language_code(hass: HomeAssistant) -> str:
|
||||
def best_matching_language_code(
|
||||
hass: HomeAssistant, assist_language: str, agent_language: str | None = None
|
||||
) -> str:
|
||||
"""Get the best matching language, based on the preferred assist language and the configured agent language."""
|
||||
"""Get the best matching language.
|
||||
|
||||
Based on the preferred assist language and the configured
|
||||
agent language.
|
||||
"""
|
||||
|
||||
# Use the assist language if supported
|
||||
if assist_language in SUPPORTED_LANGUAGE_CODES:
|
||||
|
||||
@@ -62,7 +62,8 @@ def tts_options_schema(
|
||||
"""Return schema for TTS options with default values from config or constants."""
|
||||
# If we are called from the config flow we want the defaults to be from constants
|
||||
# to allow clearing the current value (passed as suggested_value) in the UI.
|
||||
# If we aren't called from the config flow we want the defaults to be from the config.
|
||||
# If we aren't called from the config flow we want the
|
||||
# defaults to be from the config.
|
||||
defaults = {} if from_config_flow else config_options
|
||||
return vol.Schema(
|
||||
{
|
||||
|
||||
@@ -193,7 +193,8 @@ class BaseGoogleCloudProvider:
|
||||
ssml_gender=gender,
|
||||
name=voice,
|
||||
),
|
||||
# Avoid: "This voice does not support speaking rate or pitch parameters at this time."
|
||||
# Avoid: "This voice does not support speaking rate
|
||||
# or pitch parameters at this time."
|
||||
# by not specifying the fields unless they differ from the defaults
|
||||
audio_config=texttospeech.AudioConfig(
|
||||
audio_encoding=encoding,
|
||||
|
||||
@@ -42,7 +42,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleDriveConfigEntry)
|
||||
OAuth2Session(hass, entry, implementation),
|
||||
)
|
||||
|
||||
# Test we can refresh the token and raise ConfigEntryAuthFailed or ConfigEntryNotReady if not
|
||||
# Test we can refresh the token and raise
|
||||
# ConfigEntryAuthFailed or ConfigEntryNotReady if not
|
||||
await auth.async_get_access_token()
|
||||
|
||||
client = DriveClient(await instance_id.async_get(hass), auth)
|
||||
|
||||
@@ -132,7 +132,8 @@ class DriveClient:
|
||||
query = " and ".join(
|
||||
[
|
||||
"properties has { key='home_assistant' and value='root' }",
|
||||
f"properties has {{ key='instance_id' and value='{self._ha_instance_id}' }}",
|
||||
"properties has { key='instance_id'"
|
||||
f" and value='{self._ha_instance_id}' }}",
|
||||
"trashed=false",
|
||||
]
|
||||
)
|
||||
@@ -196,7 +197,8 @@ class DriveClient:
|
||||
query = " and ".join(
|
||||
[
|
||||
"properties has { key='home_assistant' and value='backup' }",
|
||||
f"properties has {{ key='instance_id' and value='{self._ha_instance_id}' }}",
|
||||
"properties has { key='instance_id'"
|
||||
f" and value='{self._ha_instance_id}' }}",
|
||||
"trashed=false",
|
||||
]
|
||||
)
|
||||
@@ -220,7 +222,8 @@ class DriveClient:
|
||||
query = " and ".join(
|
||||
[
|
||||
"properties has { key='home_assistant' and value='backup' }",
|
||||
f"properties has {{ key='instance_id' and value='{self._ha_instance_id}' }}",
|
||||
"properties has { key='instance_id'"
|
||||
f" and value='{self._ha_instance_id}' }}",
|
||||
f"properties has {{ key='backup_id' and value='{backup_id}' }}",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -86,7 +86,8 @@ class GoogleGenerativeAITaskEntity(
|
||||
|
||||
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
||||
LOGGER.error(
|
||||
"Last content in chat log is not an AssistantContent: %s. This could be due to the model not returning a valid response",
|
||||
"Last content in chat log is not an AssistantContent: %s."
|
||||
" This could be due to the model not returning a valid response",
|
||||
chat_log.content[-1],
|
||||
)
|
||||
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
|
||||
@@ -149,7 +150,9 @@ class GoogleGenerativeAITaskEntity(
|
||||
|
||||
if response.prompt_feedback:
|
||||
raise HomeAssistantError(
|
||||
f"Error generating content due to content violations, reason: {response.prompt_feedback.block_reason_message}"
|
||||
"Error generating content due to content"
|
||||
" violations, reason:"
|
||||
f" {response.prompt_feedback.block_reason_message}"
|
||||
)
|
||||
|
||||
if (
|
||||
|
||||
@@ -254,7 +254,8 @@ class LLMSubentryFlowHandler(ConfigSubentryFlow):
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if not user_input.get(CONF_LLM_HASS_API):
|
||||
user_input.pop(CONF_LLM_HASS_API, None)
|
||||
# Don't allow to save options that enable the Google Search tool with an Assist API
|
||||
# Don't allow to save options that enable the
|
||||
# Google Search tool with an Assist API
|
||||
if not (
|
||||
user_input.get(CONF_LLM_HASS_API)
|
||||
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
|
||||
|
||||
@@ -368,7 +368,9 @@ async def _transform_stream(
|
||||
thinking_content_index = 0
|
||||
tool_call_index = 0
|
||||
|
||||
# According to the API docs, this would mean no candidate is returned, so we can safely throw an error here.
|
||||
# According to the API docs, this would mean no
|
||||
# candidate is returned, so we can safely throw
|
||||
# an error here.
|
||||
if response.prompt_feedback or not response.candidates:
|
||||
reason = (
|
||||
response.prompt_feedback.block_reason_message
|
||||
@@ -376,7 +378,8 @@ async def _transform_stream(
|
||||
else "unknown"
|
||||
)
|
||||
raise HomeAssistantError(
|
||||
f"The message got blocked due to content violations, reason: {reason}"
|
||||
"The message got blocked due to content"
|
||||
f" violations, reason: {reason}"
|
||||
)
|
||||
|
||||
candidate = response.candidates[0]
|
||||
@@ -510,9 +513,11 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
for tool in chat_log.llm_api.tools
|
||||
]
|
||||
|
||||
# Using search grounding allows the model to retrieve information from the web,
|
||||
# however, it may interfere with how the model decides to use some tools, or entities
|
||||
# for example weather entity may be disregarded if the model chooses to Google it.
|
||||
# Using search grounding allows the model to retrieve
|
||||
# information from the web, however, it may interfere
|
||||
# with how the model decides to use some tools, or
|
||||
# entities for example weather entity may be
|
||||
# disregarded if the model chooses to Google it.
|
||||
if options.get(CONF_USE_GOOGLE_SEARCH_TOOL) is True:
|
||||
tools = tools or []
|
||||
tools.append(Tool(google_search=GoogleSearch()))
|
||||
@@ -548,8 +553,11 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
not isinstance(chat_content, conversation.ToolResultContent)
|
||||
and chat_content.content == ""
|
||||
):
|
||||
# Skipping is not possible since the number of function calls need to match the number of function responses
|
||||
# and skipping one would mean removing the other and hence this would prevent a proper chat log
|
||||
# Skipping is not possible since the number of
|
||||
# function calls need to match the number of
|
||||
# function responses and skipping one would
|
||||
# mean removing the other and hence this would
|
||||
# prevent a proper chat log
|
||||
chat_content = replace(chat_content, content=" ")
|
||||
|
||||
if tool_results:
|
||||
@@ -748,7 +756,9 @@ async def async_prepare_files_for_prompt(
|
||||
|
||||
if uploaded_file.state == FileState.FAILED:
|
||||
raise HomeAssistantError(
|
||||
f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message if uploaded_file.error else 'unknown'}"
|
||||
f"File `{uploaded_file.name}` processing"
|
||||
" failed, reason:"
|
||||
f" {uploaded_file.error.message if uploaded_file.error else 'unknown'}"
|
||||
)
|
||||
|
||||
prompt_parts = await hass.async_add_executor_job(upload_files)
|
||||
|
||||
@@ -59,7 +59,8 @@ def _parse_audio_mime_type(mime_type: str) -> dict[str, int]:
|
||||
for param in parts: # Skip the main type part
|
||||
param = param.strip()
|
||||
if param.lower().startswith("rate="):
|
||||
# Handle cases like "rate=" with no value or non-integer value and keep rate as default
|
||||
# Handle cases like "rate=" with no value or
|
||||
# non-integer value and keep rate as default
|
||||
with suppress(ValueError, IndexError):
|
||||
rate_str = param.split("=", 1)[1]
|
||||
rate = int(rate_str)
|
||||
|
||||
@@ -216,8 +216,10 @@ class GoogleGenerativeAISttEntity(
|
||||
@property
|
||||
def supported_channels(self) -> list[stt.AudioChannels]:
|
||||
"""Return a list of supported channels."""
|
||||
# Per https://ai.google.dev/gemini-api/docs/audio
|
||||
# If the audio source contains multiple channels, Gemini combines those channels into a single channel.
|
||||
# Per
|
||||
# https://ai.google.dev/gemini-api/docs/audio
|
||||
# If the audio source contains multiple channels,
|
||||
# Gemini combines those channels into a single channel.
|
||||
return [stt.AudioChannels.CHANNEL_MONO]
|
||||
|
||||
async def async_process_audio_stream(
|
||||
|
||||
@@ -111,7 +111,8 @@ class GooglePhotosMediaSource(MediaSource):
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media identifier to a url.
|
||||
|
||||
This will resolve a specific media item to a url for the full photo or video contents.
|
||||
This will resolve a specific media item to a url for
|
||||
the full photo or video contents.
|
||||
"""
|
||||
try:
|
||||
identifier = PhotosIdentifier.of(item.identifier)
|
||||
@@ -282,7 +283,9 @@ def _build_media_item(
|
||||
|
||||
|
||||
def _media_url(media_item: MediaItem, max_size: int) -> str:
|
||||
"""Return a media item url with the specified max thumbnail size on the longest edge.
|
||||
"""Return a media item url with the specified max thumbnail size.
|
||||
|
||||
The size applies to the longest edge.
|
||||
|
||||
See https://developers.google.com/photos/library/guides/access-media-items#base-urls
|
||||
"""
|
||||
|
||||
@@ -116,7 +116,8 @@ class AsyncConfigEntryAuth:
|
||||
def response_handler(_, response, exception: HttpError) -> None:
|
||||
if exception is not None:
|
||||
raise GoogleTasksApiError(
|
||||
f"Google Tasks API responded with error ({exception.reason or exception.status_code})"
|
||||
"Google Tasks API responded with error"
|
||||
f" ({exception.reason or exception.status_code})"
|
||||
) from exception
|
||||
if response:
|
||||
data = json.loads(response)
|
||||
|
||||
@@ -57,7 +57,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.error(
|
||||
"Invalid time format found while migrating: %s. The old config never worked. Reset to default (empty)",
|
||||
"Invalid time format found while migrating: %s."
|
||||
" The old config never worked."
|
||||
" Reset to default (empty)",
|
||||
options[CONF_TIME],
|
||||
)
|
||||
options[CONF_TIME] = None
|
||||
|
||||
@@ -97,7 +97,8 @@ def _is_location_already_configured(
|
||||
continue
|
||||
# A more accurate way is to use the haversine formula, but for simplicity
|
||||
# we use a simple distance check. The epsilon value is small anyway.
|
||||
# This is mostly to capture cases where the user has slightly moved the location pin.
|
||||
# This is mostly to capture cases where the user
|
||||
# has slightly moved the location pin.
|
||||
if (
|
||||
abs(subentry.data[CONF_LATITUDE] - new_data[CONF_LATITUDE]) <= epsilon
|
||||
and abs(subentry.data[CONF_LONGITUDE] - new_data[CONF_LONGITUDE])
|
||||
|
||||
@@ -34,9 +34,11 @@ async def async_get_config_entry_diagnostics(
|
||||
"daily_forecast_data": subentry_rt.coordinator_daily_forecast.data.to_dict()
|
||||
if subentry_rt.coordinator_daily_forecast.data
|
||||
else None,
|
||||
"hourly_forecast_data": subentry_rt.coordinator_hourly_forecast.data.to_dict()
|
||||
if subentry_rt.coordinator_hourly_forecast.data
|
||||
else None,
|
||||
"hourly_forecast_data": (
|
||||
subentry_rt.coordinator_hourly_forecast.data.to_dict()
|
||||
if subentry_rt.coordinator_hourly_forecast.data
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
return async_redact_data(diag_data, TO_REDACT)
|
||||
|
||||
@@ -264,7 +264,9 @@ class GoogleWeatherEntity(
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: (
|
||||
item.feels_like_max_temperature.degrees
|
||||
),
|
||||
ATTR_FORECAST_WIND_BEARING: item.daytime_forecast.wind.direction.degrees,
|
||||
ATTR_FORECAST_WIND_BEARING: (
|
||||
item.daytime_forecast.wind.direction.degrees
|
||||
),
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: max(
|
||||
item.daytime_forecast.wind.gust.value,
|
||||
item.nighttime_forecast.wind.gust.value,
|
||||
@@ -292,10 +294,14 @@ class GoogleWeatherEntity(
|
||||
),
|
||||
ATTR_FORECAST_TIME: item.interval.start_time,
|
||||
ATTR_FORECAST_HUMIDITY: item.relative_humidity,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: item.precipitation.probability.percent,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: (
|
||||
item.precipitation.probability.percent
|
||||
),
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: item.cloud_cover,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: item.precipitation.qpf.quantity,
|
||||
ATTR_FORECAST_NATIVE_PRESSURE: item.air_pressure.mean_sea_level_millibars,
|
||||
ATTR_FORECAST_NATIVE_PRESSURE: (
|
||||
item.air_pressure.mean_sea_level_millibars
|
||||
),
|
||||
ATTR_FORECAST_NATIVE_TEMP: item.temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item.feels_like_temperature.degrees,
|
||||
ATTR_FORECAST_WIND_BEARING: item.wind.direction.degrees,
|
||||
@@ -326,11 +332,17 @@ class GoogleWeatherEntity(
|
||||
),
|
||||
ATTR_FORECAST_TIME: day_forecast.interval.start_time,
|
||||
ATTR_FORECAST_HUMIDITY: day_forecast.relative_humidity,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: day_forecast.precipitation.probability.percent,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: (
|
||||
day_forecast.precipitation.probability.percent
|
||||
),
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: day_forecast.cloud_cover,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: day_forecast.precipitation.qpf.quantity,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: (
|
||||
day_forecast.precipitation.qpf.quantity
|
||||
),
|
||||
ATTR_FORECAST_NATIVE_TEMP: item.max_temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item.feels_like_max_temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: (
|
||||
item.feels_like_max_temperature.degrees
|
||||
),
|
||||
ATTR_FORECAST_WIND_BEARING: day_forecast.wind.direction.degrees,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: day_forecast.wind.gust.value,
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED: day_forecast.wind.speed.value,
|
||||
@@ -348,13 +360,21 @@ class GoogleWeatherEntity(
|
||||
),
|
||||
ATTR_FORECAST_TIME: night_forecast.interval.start_time,
|
||||
ATTR_FORECAST_HUMIDITY: night_forecast.relative_humidity,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: night_forecast.precipitation.probability.percent,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: (
|
||||
night_forecast.precipitation.probability.percent
|
||||
),
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: night_forecast.cloud_cover,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: night_forecast.precipitation.qpf.quantity,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: (
|
||||
night_forecast.precipitation.qpf.quantity
|
||||
),
|
||||
ATTR_FORECAST_NATIVE_TEMP: item.min_temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item.feels_like_min_temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: (
|
||||
item.feels_like_min_temperature.degrees
|
||||
),
|
||||
ATTR_FORECAST_WIND_BEARING: night_forecast.wind.direction.degrees,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: night_forecast.wind.gust.value,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: (
|
||||
night_forecast.wind.gust.value
|
||||
),
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED: night_forecast.wind.speed.value,
|
||||
ATTR_FORECAST_UV_INDEX: night_forecast.uv_index,
|
||||
ATTR_FORECAST_IS_DAYTIME: False,
|
||||
|
||||
@@ -94,7 +94,10 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): vol.In(
|
||||
{
|
||||
address: f"{device.get_device_name(None) or discovery_info.name} ({address})"
|
||||
address: (
|
||||
f"{device.get_device_name(None) or discovery_info.name}"
|
||||
f" ({address})"
|
||||
)
|
||||
for address, (
|
||||
device,
|
||||
discovery_info,
|
||||
|
||||
@@ -27,7 +27,10 @@ def process_service_info(
|
||||
entry: GoveeBLEConfigEntry,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
) -> SensorUpdate:
|
||||
"""Process a BluetoothServiceInfoBleak, running side effects and returning sensor data."""
|
||||
"""Process a BluetoothServiceInfoBleak.
|
||||
|
||||
Runs side effects and returns sensor data.
|
||||
"""
|
||||
coordinator = entry.runtime_data
|
||||
data = coordinator.device_data
|
||||
update = data.update(service_info)
|
||||
|
||||
@@ -94,7 +94,8 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
f"Device {self.name} is unavailable, could not send update request"
|
||||
) from error
|
||||
else:
|
||||
# raise update failed if time for more than MAX_ERRORS has passed since last update
|
||||
# raise update failed if time for more than
|
||||
# MAX_ERRORS has passed since last update
|
||||
now = utcnow()
|
||||
elapsed_success = now - self._last_response_time
|
||||
if self.update_interval and elapsed_success >= timedelta(
|
||||
@@ -115,7 +116,8 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self._error_count = 0
|
||||
if self.last_update_success and self._error_count >= MAX_ERRORS:
|
||||
raise UpdateFailed(
|
||||
f"Device {self.name} is unresponsive for too long and now unavailable"
|
||||
f"Device {self.name} is unresponsive for"
|
||||
" too long and now unavailable"
|
||||
)
|
||||
|
||||
self._last_response_time = utcnow()
|
||||
|
||||
@@ -142,7 +142,8 @@ class LockGroup(GroupEntity, LockEntity):
|
||||
self._attr_is_unlocking = None
|
||||
self._attr_is_locked = None
|
||||
else:
|
||||
# Set attributes based on member states and let the lock entity sort out the correct state
|
||||
# Set attributes based on member states and let the
|
||||
# lock entity sort out the correct state
|
||||
self._attr_is_jammed = LockState.JAMMED in states
|
||||
self._attr_is_locking = LockState.LOCKING in states
|
||||
self._attr_is_opening = LockState.OPENING in states
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Platform allowing several sensors to be grouped into one sensor to provide numeric combinations."""
|
||||
"""Platform allowing several sensors to be grouped into one sensor.
|
||||
|
||||
Provides numeric combinations.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
@@ -399,7 +402,8 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
numeric_state = float(state.state)
|
||||
uom = state.attributes.get("unit_of_measurement")
|
||||
|
||||
# Convert the state to the native unit of measurement when we have valid units
|
||||
# Convert the state to the native unit of
|
||||
# measurement when we have valid units
|
||||
# and a correct device class
|
||||
if valid_units and uom in valid_units and self._can_convert is True:
|
||||
numeric_state = UNIT_CONVERTERS[self.device_class].convert(
|
||||
@@ -439,10 +443,14 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
if entity_id not in self._state_incorrect:
|
||||
self._state_incorrect.add(entity_id)
|
||||
_LOGGER.warning(
|
||||
"Unable to use state. Only entities with correct unit of measurement"
|
||||
"Unable to use state. Only entities"
|
||||
" with correct unit of measurement"
|
||||
" is supported,"
|
||||
" entity %s, value %s with device class %s"
|
||||
" and unit of measurement %s excluded from calculation in %s",
|
||||
" entity %s, value %s with"
|
||||
" device class %s"
|
||||
" and unit of measurement %s"
|
||||
" excluded from calculation"
|
||||
" in %s",
|
||||
entity_id,
|
||||
state.state,
|
||||
self.device_class,
|
||||
@@ -507,7 +515,8 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
if not self._ignore_non_numeric and len(valid_state_entities) < len(
|
||||
self._entity_ids
|
||||
):
|
||||
# Only return state class if all states are valid when not ignoring non numeric
|
||||
# Only return state class if all states are valid
|
||||
# when not ignoring non numeric
|
||||
return None
|
||||
|
||||
state_classes: list[SensorStateClass] = []
|
||||
@@ -562,7 +571,8 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
if not self._ignore_non_numeric and len(valid_state_entities) < len(
|
||||
self._entity_ids
|
||||
):
|
||||
# Only return device class if all states are valid when not ignoring non numeric
|
||||
# Only return device class if all states are valid
|
||||
# when not ignoring non numeric
|
||||
return None
|
||||
|
||||
device_classes: list[SensorDeviceClass] = []
|
||||
@@ -618,7 +628,8 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
if not self._ignore_non_numeric and len(valid_state_entities) < len(
|
||||
self._entity_ids
|
||||
):
|
||||
# Only return device class if all states are valid when not ignoring non numeric
|
||||
# Only return device class if all states are valid
|
||||
# when not ignoring non numeric
|
||||
return None
|
||||
|
||||
unit_of_measurements: list[str] = []
|
||||
@@ -631,7 +642,8 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return None
|
||||
unit_of_measurements.append(_unit_of_measurement)
|
||||
|
||||
# Ensure only valid unit of measurements for the specific device class can be used
|
||||
# Ensure only valid unit of measurements for the
|
||||
# specific device class can be used
|
||||
if (
|
||||
(
|
||||
# Test if uom's in device class is convertible
|
||||
|
||||
@@ -32,7 +32,7 @@ def mean_tuple(*args: Any) -> tuple[float | Any, ...]:
|
||||
|
||||
|
||||
def mean_circle(*args: Any) -> tuple[float | Any, ...]:
|
||||
"""Return the circular mean of hue values and arithmetic mean of saturation values from HS color tuples."""
|
||||
"""Return circular mean of hue and arithmetic mean of saturation from HS tuples."""
|
||||
if not args:
|
||||
return ()
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@ async def async_migrate_entry(
|
||||
achieve:
|
||||
Migration: login() → plant_list() → [cache API instance]
|
||||
Setup: [reuse cached API] → device_list()
|
||||
This reduces to just 1 login() call during the migration+setup cycle and prevent account lockout.
|
||||
This reduces to just 1 login() call during the
|
||||
migration+setup cycle and prevent account lockout.
|
||||
"""
|
||||
_LOGGER.debug(
|
||||
"Migrating config entry from version %s.%s",
|
||||
@@ -125,7 +126,8 @@ async def async_migrate_entry(
|
||||
|
||||
# Handle DEFAULT_PLANT_ID resolution
|
||||
if config.get(CONF_PLANT_ID) == DEFAULT_PLANT_ID:
|
||||
# V1 API should never have DEFAULT_PLANT_ID (plant selection happens in config flow)
|
||||
# V1 API should never have DEFAULT_PLANT_ID
|
||||
# (plant selection happens in config flow)
|
||||
# If it does, this indicates a corrupted config entry
|
||||
if config.get(CONF_AUTH_TYPE) == AUTH_API_TOKEN:
|
||||
_LOGGER.error(
|
||||
@@ -260,7 +262,8 @@ def get_device_list_v1(
|
||||
f"Authentication failed for Growatt API: {e.error_msg or str(e)}"
|
||||
) from e
|
||||
raise ConfigEntryError(
|
||||
f"API error during device list: {e.error_msg or str(e)} (Code: {e.error_code})"
|
||||
f"API error during device list: {e.error_msg or str(e)}"
|
||||
f" (Code: {e.error_code})"
|
||||
) from e
|
||||
devices = devices_dict.get("devices", [])
|
||||
supported_devices = [
|
||||
|
||||
@@ -152,7 +152,8 @@ class GrowattServerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = ERROR_INVALID_AUTH
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Growatt V1 API error during credential update: %s (Code: %s)",
|
||||
"Growatt V1 API error during credential"
|
||||
" update: %s (Code: %s)",
|
||||
err.error_msg or str(err),
|
||||
err.error_code,
|
||||
)
|
||||
|
||||
@@ -104,11 +104,16 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
if self.device_type == "total":
|
||||
if self.api_version == "v1":
|
||||
# The V1 Plant APIs do not provide the same information as the classic plant_info() API
|
||||
# The V1 Plant APIs do not provide the same
|
||||
# information as the classic plant_info() API
|
||||
# More specifically:
|
||||
# 1. There is no monetary information to be found, so today and lifetime money is not available
|
||||
# 2. There is no nominal power, this is provided by inverter min_energy()
|
||||
# This means, for the total coordinator we can only fetch and map the following:
|
||||
# 1. There is no monetary information to be
|
||||
# found, so today and lifetime money is not
|
||||
# available
|
||||
# 2. There is no nominal power, this is
|
||||
# provided by inverter min_energy()
|
||||
# This means, for the total coordinator we can
|
||||
# only fetch and map the following:
|
||||
# todayEnergy -> today_energy
|
||||
# totalEnergy -> total_energy
|
||||
# invTodayPpv -> current_power
|
||||
@@ -117,7 +122,8 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
except growattServer.GrowattV1ApiError as err:
|
||||
if err.error_code == V1_API_ERROR_NO_PRIVILEGE:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Authentication failed for Growatt API: {err.error_msg or str(err)}"
|
||||
"Authentication failed for Growatt API:"
|
||||
f" {err.error_msg or str(err)}"
|
||||
) from err
|
||||
raise UpdateFailed(
|
||||
f"Error fetching plant energy overview: {err}"
|
||||
@@ -145,7 +151,8 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
except growattServer.GrowattV1ApiError as err:
|
||||
if err.error_code == V1_API_ERROR_NO_PRIVILEGE:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Authentication failed for Growatt API: {err.error_msg or str(err)}"
|
||||
"Authentication failed for Growatt API:"
|
||||
f" {err.error_msg or str(err)}"
|
||||
) from err
|
||||
raise UpdateFailed(f"Error fetching min device data: {err}") from err
|
||||
|
||||
@@ -172,7 +179,8 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
except growattServer.GrowattV1ApiError as err:
|
||||
if err.error_code == V1_API_ERROR_NO_PRIVILEGE:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Authentication failed for Growatt API: {err.error_msg or str(err)}"
|
||||
"Authentication failed for Growatt API:"
|
||||
f" {err.error_msg or str(err)}"
|
||||
) from err
|
||||
raise UpdateFailed(f"Error fetching SPH device data: {err}") from err
|
||||
|
||||
@@ -376,7 +384,8 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
try:
|
||||
# Use V1 API for token authentication
|
||||
# The library's _process_response will raise GrowattV1ApiError if error_code != 0
|
||||
# The library's _process_response will raise
|
||||
# GrowattV1ApiError if error_code != 0
|
||||
await self.hass.async_add_executor_job(
|
||||
self.api.min_write_time_segment,
|
||||
self.device_id,
|
||||
@@ -393,7 +402,8 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
# Update coordinator's cached data without making an API call (avoids rate limit)
|
||||
# Update coordinator's cached data without making an
|
||||
# API call (avoids rate limit)
|
||||
if self.data:
|
||||
# Update the time segment data in the cache
|
||||
self.data[f"forcedTimeStart{segment_id}"] = start_time.strftime("%H:%M")
|
||||
|
||||
@@ -242,7 +242,8 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
# This sensor is manually created using the most recent X-Axis value from the chartData
|
||||
# This sensor is manually created using the most recent
|
||||
# X-Axis value from the chartData
|
||||
GrowattSensorEntityDescription(
|
||||
key="mix_last_update",
|
||||
translation_key="mix_last_update",
|
||||
@@ -253,7 +254,9 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
GrowattSensorEntityDescription(
|
||||
key="mix_import_from_grid_today_combined",
|
||||
translation_key="mix_import_from_grid_today_combined",
|
||||
api_key="etouser_combined", # This id is not present in the raw API data, it is added by the sensor
|
||||
# This id is not present in the raw API data,
|
||||
# it is added by the sensor
|
||||
api_key="etouser_combined",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
|
||||
@@ -188,7 +188,8 @@ class PairedSensorManager:
|
||||
try:
|
||||
uids = set(self._sensor_pair_dump_coordinator.data["paired_uids"])
|
||||
except KeyError:
|
||||
# Sometimes the paired_uids key can fail to exist; the user can't do anything
|
||||
# Sometimes the paired_uids key can fail to exist;
|
||||
# the user can't do anything
|
||||
# about it, so in this case, we quietly abort and return:
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user