1
0
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:
Franck Nijhof
2026-05-14 22:29:56 +02:00
committed by GitHub
parent e5682d97a6
commit 2ec51ef113
85 changed files with 413 additions and 172 deletions
@@ -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 (
+3 -2
View File
@@ -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)
+5 -2
View File
@@ -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
+5 -2
View File
@@ -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"
+2 -1
View File
@@ -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
+4 -1
View File
@@ -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:
+2 -1
View File
@@ -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
+10 -2
View File
@@ -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
+4 -2
View File
@@ -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,
+12 -6
View File
@@ -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(
+10 -4
View File
@@ -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
+1 -1
View File
@@ -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:
+2 -1
View File
@@ -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(
+1 -1
View File
@@ -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,
+5 -3
View File
@@ -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)
+8 -3
View File
@@ -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
+2 -1
View File
@@ -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()
+4 -1
View File
@@ -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))},
+4 -2
View File
@@ -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}"
+2 -1
View File
@@ -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
+7 -4
View File
@@ -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",
],
+4 -1
View File
@@ -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(
+2 -1
View File
@@ -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,
)
+12 -5
View File
@@ -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_)
+2 -1
View File
@@ -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(
{
+2 -1
View File
@@ -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)
+6 -3
View File
@@ -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
"""
+2 -1
View File
@@ -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)
+4 -2
View File
@@ -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()
+2 -1
View File
@@ -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
+21 -9
View File
@@ -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
+1 -1
View File
@@ -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