Compare commits

..

29 Commits

Author SHA1 Message Date
Nikolai Rahimi 44f5ad84b9 Bump mitsubishi-comfort to 0.3.2 (#174100)
Co-authored-by: Nikolai Rahimi <nikolairahimi@users.noreply.github.com>
2026-06-17 16:22:08 +02:00
some-random-climber 7330c25685 Remove battery_level attribute from icloud device tracker (#174117) 2026-06-17 16:09:15 +02:00
Leo Periou e59086d299 Change myneomitis codeowner (#174130) 2026-06-17 15:54:54 +02:00
Manu 27f44f83fb Add reauthentication flow to SMTP (#174092)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-17 13:59:43 +02:00
Simone Chemelli 05c94fa578 Bump aioamazondevices to 14.1.2 (#174114)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-06-17 13:48:42 +02:00
Tom 64b608b439 Set parallel updates for ProxmoxVE buttons (#174125) 2026-06-17 13:33:13 +02:00
Dellle c5b90cf8d1 Bump elevenlabs to 2.51.0 (#174112)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-17 13:11:30 +02:00
Marc Hörsken 31259725ec Add support for slat-based WMS covers like venetian blinds (#145005)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-06-17 13:10:53 +02:00
Jan Bouwhuis 8ad7c12405 Fix MQTT discovery option unjustly added to entry data (#174073) 2026-06-17 12:16:29 +02:00
Martin Hjelmare 66f0f170b7 Add pylint naive_now checker (#174053)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-06-17 11:44:42 +02:00
some-random-climber da91865130 Remove battery_level attribute from starline device tracker (#174118) 2026-06-17 11:10:36 +02:00
Mark 3437bcfb42 Add Rabbit Air air quality sensor (#172993) 2026-06-17 10:49:53 +02:00
Åke Strandberg 1fd5d0a5fd Aqvify reaches Platinum tier (#174111) 2026-06-17 10:49:41 +02:00
Dellle 404c58435a Bump sentence-stream to 1.3.0 (#174113) 2026-06-17 10:48:42 +02:00
Erik Montnemery 7aba1daa16 Adjust language in condition history manager comments (#174106) 2026-06-17 09:23:32 +02:00
Jan Bouwhuis 12397cc4c1 Rename advanced settings/options in MQTT subentry translation strings (#174071) 2026-06-17 09:22:25 +02:00
Josef Zweck 73cdf7e067 Revert "Add pyserial-asyncio and pyserial-asyncio-fast to deprecated packages" (#174110) 2026-06-17 09:18:44 +02:00
Franck Nijhof 4e2cfecd96 Filter out closed sites in Amber Electric config flow (#174084) 2026-06-17 09:18:19 +02:00
Åke Strandberg 4625f7de27 Aqvify has reached gold tier (#174018) 2026-06-17 09:11:42 +02:00
Brett Adams 53a1db405c Improve test coverage of Teslemetry offline polling (#174108)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 08:47:08 +02:00
Paul Bottein ff7262d36f Fix Yoto quality scale comments (#174088) 2026-06-17 08:43:08 +02:00
tronikos 54feb95b76 Gemini: Update TTS model to gemini-3.1 and adjust configuration options (#174094) 2026-06-17 08:42:00 +02:00
epenet d9e2b49c0c Fix incorrect use of entity component constants in template (#172532) 2026-06-17 07:55:57 +02:00
renovate[bot] 4f9051464d Update cryptography to 48.0.1 (#174096) 2026-06-17 07:34:00 +02:00
Paulus Schoutsen 87894fd623 Activate venv before running python commands (#174093) 2026-06-17 07:32:22 +02:00
Franck Nijhof 34a70a9210 Clean up deprecated solar_rising entity from sun integration (#174079) 2026-06-17 06:44:16 +02:00
Paulus Schoutsen c9fb6a13fb Remove stale requirements_test_all.txt reference (#174095) 2026-06-17 05:08:20 +02:00
Franck Nijhof 1601b5151c Bump opower to 0.18.5 (#174080) 2026-06-16 14:20:30 -07:00
Franck Nijhof da0e23093d Cast system version to string for simplisafe device model (#174081) 2026-06-16 22:05:31 +02:00
178 changed files with 3009 additions and 719 deletions
Generated
+2 -2
View File
@@ -1167,8 +1167,8 @@ CLAUDE.md @home-assistant/core
/tests/components/mutesync/ @currentoor
/homeassistant/components/my/ @home-assistant/core
/tests/components/my/ @home-assistant/core
/homeassistant/components/myneomitis/ @l-pr
/tests/components/myneomitis/ @l-pr
/homeassistant/components/myneomitis/ @Epyes
/tests/components/myneomitis/ @Epyes
/homeassistant/components/mysensors/ @MartinHjelmare @functionpointer
/tests/components/mysensors/ @MartinHjelmare @functionpointer
/homeassistant/components/mystrom/ @fabaff
+1 -1
View File
@@ -210,7 +210,7 @@ async def async_generate_image(
source = hass.data[DATA_MEDIA_SOURCE]
current_time = datetime.now()
current_time = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
ext = mimetypes.guess_extension(task_result.mime_type, False) or ".png"
sanitized_task_name = RE_SANITIZE_FILENAME.sub("", slugify(task_name))
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==14.0.4"]
"requirements": ["aioamazondevices==14.1.2"]
}
@@ -34,11 +34,13 @@ def generate_site_selector_name(site: Site) -> str:
def filter_sites(sites: list[Site]) -> list[Site]:
"""Deduplicates the list of sites."""
"""Filter out closed sites and deduplicate the list of sites."""
filtered: list[Site] = []
filtered_nmi: set[str] = set()
for site in sorted(sites, key=lambda site: site.status):
if site.status == SiteStatus.CLOSED:
continue
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
filtered.append(site)
filtered_nmi.add(site.nmi)
@@ -75,7 +75,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
**user_input,
CONF_BRAND: user_input[CONF_BRAND],
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
},
)
@@ -119,7 +119,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
data_updates={
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
},
)
@@ -71,7 +71,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
+ REFRESH_TOKEN_EXPIRY_TIME.total_seconds()
)
try:
if datetime.now().timestamp() >= expiry_time:
if datetime.now().timestamp() >= expiry_time: # pylint: disable=home-assistant-enforce-naive-now
await self._reauthenticate()
else:
await self.aquacell_api.authenticate_refresh(self.refresh_token)
@@ -92,7 +92,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
data = {
**self.config_entry.data,
CONF_REFRESH_TOKEN: self.refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
}
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pyaqvify"],
"quality_scale": "silver",
"quality_scale": "platinum",
"requirements": ["pyaqvify==0.0.11"]
}
@@ -53,29 +53,43 @@ rules:
test-coverage: done
# Gold
devices: todo
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: |
Discovery not possible, as device is connected via 4G only. No LAN connection.
discovery:
status: exempt
comment: |
Discovery not possible, as device is connected via 4G only. No LAN connection.
docs-data-update: done
docs-examples: done
docs-known-limitations:
status: done
comment: |
No known limitations
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
entity-category:
status: done
comment: |
None of current sensors should be set as diagnostic
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
reconfiguration-flow: done
repair-issues:
status: exempt
comment: |
No repair issues are created.
stale-devices: done
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
async-dependency: done
inject-websession: done
strict-typing: done
+1 -1
View File
@@ -61,7 +61,7 @@ def _activity_time_based(latest: Activity) -> Activity | None:
"""Get the latest state of the sensor."""
start = latest.activity_start_time
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
if start <= datetime.now() <= end:
if start <= datetime.now() <= end: # pylint: disable=home-assistant-enforce-naive-now
return latest
return None
@@ -239,11 +239,11 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
self.source = None
if start_datetime := playing_info.get("startDateTime"):
start_datetime = datetime.fromisoformat(start_datetime)
current_datetime = datetime.now().replace(tzinfo=start_datetime.tzinfo)
current_datetime = datetime.now().replace(tzinfo=start_datetime.tzinfo) # pylint: disable=home-assistant-enforce-naive-now
self.media_position = int(
(current_datetime - start_datetime).total_seconds()
)
self.media_position_updated_at = datetime.now()
self.media_position_updated_at = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
else:
self.media_position = None
self.media_position_updated_at = None
@@ -31,7 +31,7 @@ class BroadlinkHeartbeat:
async def async_setup(self) -> None:
"""Set up the heartbeat."""
if self._unsubscribe is None:
await self.async_heartbeat(dt.datetime.now())
await self.async_heartbeat(dt.datetime.now()) # pylint: disable=home-assistant-enforce-naive-now
self._unsubscribe = event.async_track_time_interval(
self._hass, self.async_heartbeat, self.HEARTBEAT_INTERVAL
)
+1 -1
View File
@@ -158,7 +158,7 @@ class BrData:
_LOGGER.debug("Buienradar parsed data: %s", result)
if result.get(SUCCESS) is not True:
if int(datetime.now().strftime("%H")) > 0:
if int(datetime.now().strftime("%H")) > 0: # pylint: disable=home-assistant-enforce-naive-now
_LOGGER.warning(
"Unable to parse data from Buienradar. (Msg: %s)",
result.get(MESSAGE),
@@ -546,11 +546,6 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
"%s: Dropping sample as source total_increasing sensor decreased",
self.entity_id,
)
self._last_valid_state_time = (
new_state.state,
new_timestamp,
)
self._write_native_value(Decimal(0))
return
# add latest derivative to the window list
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["elevenlabs"],
"requirements": ["elevenlabs==2.3.0", "sentence-stream==1.2.0"]
"requirements": ["elevenlabs==2.51.0", "sentence-stream==1.3.0"]
}
@@ -91,7 +91,7 @@ class FireflyDataUpdateCoordinator(DataUpdateCoordinator[FireflyCoordinatorData]
async def _async_update_data(self) -> FireflyCoordinatorData:
"""Fetch data from Firefly III API."""
now = datetime.now()
now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
end_date = now
+1 -1
View File
@@ -200,7 +200,7 @@ class FreeboxRouter:
"IPv6": connection_datas.get("ipv6"),
"connection_type": connection_datas["media"],
"uptime": datetime.fromtimestamp(
round(datetime.now().timestamp()) - syst_datas["uptime_val"]
round(datetime.now().timestamp()) - syst_datas["uptime_val"] # pylint: disable=home-assistant-enforce-naive-now
),
"firmware_version": self._sw_v,
"serial": syst_datas["serial"],
+1 -1
View File
@@ -54,7 +54,7 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
if (
self.fyta.expiration is None
or self.fyta.expiration.timestamp() < datetime.now().timestamp()
or self.fyta.expiration.timestamp() < datetime.now().timestamp() # pylint: disable=home-assistant-enforce-naive-now
):
await self.renew_authentication()
+1 -1
View File
@@ -117,5 +117,5 @@ class FytaPlantImageEntity(FytaPlantEntity, ImageEntity):
if url != self._attr_image_url:
self._cached_image = None
self._attr_image_last_updated = datetime.now()
self._attr_image_last_updated = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
return url
+2 -2
View File
@@ -154,12 +154,12 @@ class GenericCamera(Camera):
self._last_image is not None
and url == self._last_url
and self._last_update + timedelta(0, self._attr_frame_interval)
> datetime.now()
> datetime.now() # pylint: disable=home-assistant-enforce-naive-now
):
return self._last_image
try:
update_time = datetime.now()
update_time = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
async_client = get_async_client(self.hass, verify_ssl=self.verify_ssl)
response = await async_client.get(
url,
@@ -584,7 +584,7 @@ async def ws_start_preview(
if user_input.get(CONF_STILL_IMAGE_URL):
ha_still_url = (
"/api/generic/preview_flow_image"
f"/{msg['flow_id']}?t={datetime.now().isoformat()}"
f"/{msg['flow_id']}?t={datetime.now().isoformat()}" # pylint: disable=home-assistant-enforce-naive-now
)
_LOGGER.debug("Got preview still URL: %s", ha_still_url)
+1 -1
View File
@@ -29,7 +29,7 @@ SYNCHRONIZE_CLOCK = GoodweButtonEntityDescription(
key="synchronize_clock",
translation_key="synchronize_clock",
entity_category=EntityCategory.CONFIG,
action=lambda inv: inv.write_setting("time", datetime.now()),
action=lambda inv: inv.write_setting("time", datetime.now()), # pylint: disable=home-assistant-enforce-naive-now
)
+1 -1
View File
@@ -104,7 +104,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bo
# Force a token refresh to fix a bug where tokens were persisted with
# expires_in (relative time delta) and expires_at (absolute time) swapped.
# A google session token typically only lasts a few days between refresh.
now = datetime.now()
now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
if session.token["expires_at"] >= (now + timedelta(days=365)).timestamp():
session.token["expires_in"] = 0
session.token["expires_at"] = now.timestamp()
@@ -45,7 +45,7 @@ async def async_get_config_entry_diagnostics(
payload: dict[str, Any] = {
"now": dt_util.now().isoformat(),
"timezone": str(dt_util.get_default_time_zone()),
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo),
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo), # pylint: disable=home-assistant-enforce-naive-now
}
store = config_entry.runtime_data.store
@@ -434,49 +434,56 @@ async def google_generative_ai_config_option_schema(
description={"suggested_value": options.get(CONF_TEMPERATURE)},
default=RECOMMENDED_TEMPERATURE,
): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)),
vol.Optional(
CONF_TOP_P,
description={"suggested_value": options.get(CONF_TOP_P)},
default=RECOMMENDED_TOP_P,
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
vol.Optional(
CONF_TOP_K,
description={"suggested_value": options.get(CONF_TOP_K)},
default=RECOMMENDED_TOP_K,
): int,
vol.Optional(
CONF_MAX_TOKENS,
description={"suggested_value": options.get(CONF_MAX_TOKENS)},
default=RECOMMENDED_MAX_TOKENS,
): int,
vol.Optional(
CONF_HARASSMENT_BLOCK_THRESHOLD,
description={
"suggested_value": options.get(CONF_HARASSMENT_BLOCK_THRESHOLD)
},
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
): harm_block_thresholds_selector,
vol.Optional(
CONF_HATE_BLOCK_THRESHOLD,
description={"suggested_value": options.get(CONF_HATE_BLOCK_THRESHOLD)},
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
): harm_block_thresholds_selector,
vol.Optional(
CONF_SEXUAL_BLOCK_THRESHOLD,
description={
"suggested_value": options.get(CONF_SEXUAL_BLOCK_THRESHOLD)
},
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
): harm_block_thresholds_selector,
vol.Optional(
CONF_DANGEROUS_BLOCK_THRESHOLD,
description={
"suggested_value": options.get(CONF_DANGEROUS_BLOCK_THRESHOLD)
},
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
): harm_block_thresholds_selector,
}
)
if subentry_type != "tts":
schema.update(
{
vol.Optional(
CONF_TOP_P,
description={"suggested_value": options.get(CONF_TOP_P)},
default=RECOMMENDED_TOP_P,
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
vol.Optional(
CONF_TOP_K,
description={"suggested_value": options.get(CONF_TOP_K)},
default=RECOMMENDED_TOP_K,
): int,
vol.Optional(
CONF_MAX_TOKENS,
description={"suggested_value": options.get(CONF_MAX_TOKENS)},
default=RECOMMENDED_MAX_TOKENS,
): int,
vol.Optional(
CONF_HARASSMENT_BLOCK_THRESHOLD,
description={
"suggested_value": options.get(CONF_HARASSMENT_BLOCK_THRESHOLD)
},
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
): harm_block_thresholds_selector,
vol.Optional(
CONF_HATE_BLOCK_THRESHOLD,
description={
"suggested_value": options.get(CONF_HATE_BLOCK_THRESHOLD)
},
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
): harm_block_thresholds_selector,
vol.Optional(
CONF_SEXUAL_BLOCK_THRESHOLD,
description={
"suggested_value": options.get(CONF_SEXUAL_BLOCK_THRESHOLD)
},
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
): harm_block_thresholds_selector,
vol.Optional(
CONF_DANGEROUS_BLOCK_THRESHOLD,
description={
"suggested_value": options.get(CONF_DANGEROUS_BLOCK_THRESHOLD)
},
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
): harm_block_thresholds_selector,
}
)
if subentry_type == "conversation":
schema.update(
{
@@ -21,7 +21,7 @@ CONF_RECOMMENDED = "recommended"
CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "models/gemini-3.1-flash-lite"
RECOMMENDED_STT_MODEL = RECOMMENDED_CHAT_MODEL
RECOMMENDED_TTS_MODEL = "models/gemini-2.5-flash-preview-tts"
RECOMMENDED_TTS_MODEL = "models/gemini-3.1-flash-tts-preview"
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image"
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0
@@ -18,7 +18,13 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_CHAT_MODEL, LOGGER, RECOMMENDED_TTS_MODEL
from .const import (
CONF_CHAT_MODEL,
CONF_TEMPERATURE,
LOGGER,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_TTS_MODEL,
)
from .entity import GoogleGenerativeAILLMBaseEntity
from .helpers import convert_to_wav
@@ -191,7 +197,10 @@ class GoogleGenerativeAITextToSpeechEntity(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load tts audio file from the engine."""
config = self.create_generate_content_config()
config = types.GenerateContentConfig()
config.temperature = self.subentry.data.get(
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
)
config.response_modalities = ["AUDIO"]
config.speech_config = types.SpeechConfig(
voice_config=types.VoiceConfig(
@@ -69,7 +69,7 @@ def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
add_created_column = call.data[ADD_CREATED_COLUMN]
now = str(datetime.now())
now = str(datetime.now()) # pylint: disable=home-assistant-enforce-naive-now
rows = []
for d in call.data[DATA]:
row_data = ({"created": now} | d) if add_created_column else d
@@ -128,7 +128,7 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
"""
if not super().available:
return False
return datetime.now() - self._device.lastseen < DEVICE_TIMEOUT
return datetime.now() - self._device.lastseen < DEVICE_TIMEOUT # pylint: disable=home-assistant-enforce-naive-now
@property
def is_on(self) -> bool:
@@ -387,6 +387,6 @@ def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:
def next_datetime(simple_time: time) -> datetime:
"""Take a time like 08:00:00 and combine it with the current date."""
combined = datetime.combine(dt_util.start_of_local_day(), simple_time)
if combined < datetime.now():
if combined < datetime.now(): # pylint: disable=home-assistant-enforce-naive-now
combined = combined + timedelta(days=1)
return combined
@@ -75,7 +75,7 @@ def format_last_reset_elapsed_seconds(value: str | None) -> datetime | None:
if value is None:
return None
try:
last_reset = datetime.now() - timedelta(seconds=int(value))
last_reset = datetime.now() - timedelta(seconds=int(value)) # pylint: disable=home-assistant-enforce-naive-now
last_reset.replace(microsecond=0)
except ValueError:
return None
@@ -87,11 +87,6 @@ class IcloudTrackerEntity(TrackerEntity):
assert self._device.location is not None
return self._device.location[DEVICE_LOCATION_LONGITUDE]
@property
def battery_level(self) -> int | None:
"""Return the battery level of the device."""
return self._device.battery_level
@property
def icon(self) -> str:
"""Return the icon."""
+1 -1
View File
@@ -342,7 +342,7 @@ class iOSIdentifyDeviceView(HomeAssistantView):
hass = request.app[KEY_HASS]
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat()
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat() # pylint: disable=home-assistant-enforce-naive-now
device_id = data[ATTR_DEVICE_ID]
@@ -71,8 +71,8 @@ class IsraelRailDataUpdateCoordinator(DataUpdateCoordinator[list[DataConnection]
self._train_schedule.query,
self._start,
self._destination,
datetime.now().strftime("%Y-%m-%d"),
datetime.now().strftime("%H:%M"),
datetime.now().strftime("%Y-%m-%d"), # pylint: disable=home-assistant-enforce-naive-now
datetime.now().strftime("%H:%M"), # pylint: disable=home-assistant-enforce-naive-now
)
except Exception as e:
raise UpdateFailed(
@@ -214,7 +214,7 @@ class LgTVDevice(MediaPlayerEntity):
def media_image_url(self):
"""URL for obtaining a screen capture."""
return (
f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}"
f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}" # pylint: disable=home-assistant-enforce-naive-now
)
def turn_off(self) -> None:
@@ -18,7 +18,7 @@ async def async_get_config_entry_diagnostics(
payload: dict[str, Any] = {
"now": dt_util.now().isoformat(),
"timezone": str(dt_util.get_default_time_zone()),
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo),
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo), # pylint: disable=home-assistant-enforce-naive-now
}
store = config_entry.runtime_data
ics = await store.async_load()
@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["mitsubishi-comfort==0.3.1"]
"requirements": ["mitsubishi-comfort==0.3.2"]
}
@@ -68,7 +68,7 @@ class MonarchMoneyDataUpdateCoordinator(DataUpdateCoordinator[MonarchData]):
async def _async_update_data(self) -> MonarchData:
"""Fetch data for all accounts."""
now = datetime.now()
now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
account_data, cashflow_summary = await asyncio.gather(
self.client.get_accounts_as_dict_with_id_key(),
+21 -23
View File
@@ -1124,7 +1124,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]:
if user_data.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) >= user_data.get(
CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN
):
errors["advanced_settings"] = "max_below_min_kelvin"
errors["other_settings"] = "max_below_min_kelvin"
return errors
@@ -1217,7 +1217,7 @@ def validate_text_platform_config(
and CONF_MAX in config
and config[CONF_MIN] > config[CONF_MAX]
):
errors["text_advanced_settings"] = "max_below_min"
errors["text_other_settings"] = "max_below_min"
return errors
@@ -1506,7 +1506,7 @@ PLATFORM_ENTITY_FIELDS: dict[Platform, dict[str, PlatformField]] = {
selector=SUGGESTED_DISPLAY_PRECISION_SELECTOR,
required=False,
validator=cv.positive_int,
section="advanced_settings",
section="other_settings",
),
CONF_OPTIONS: PlatformField(
selector=OPTIONS_SELECTOR,
@@ -1678,13 +1678,13 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
selector=TIMEOUT_SELECTOR,
required=False,
validator=cv.positive_int,
section="advanced_settings",
section="other_settings",
),
CONF_OFF_DELAY: PlatformField(
selector=TIMEOUT_SELECTOR,
required=False,
validator=cv.positive_int,
section="advanced_settings",
section="other_settings",
),
},
Platform.BUTTON: {
@@ -3125,7 +3125,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
default=False,
validator=cv.boolean,
conditions=({CONF_SCHEMA: "json"},),
section="advanced_settings",
section="other_settings",
),
CONF_FLASH_TIME_SHORT: PlatformField(
selector=FLASH_TIME_SELECTOR,
@@ -3133,7 +3133,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
validator=cv.positive_int,
default=2,
conditions=({CONF_SCHEMA: "json"},),
section="advanced_settings",
section="other_settings",
),
CONF_FLASH_TIME_LONG: PlatformField(
selector=FLASH_TIME_SELECTOR,
@@ -3141,7 +3141,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
validator=cv.positive_int,
default=10,
conditions=({CONF_SCHEMA: "json"},),
section="advanced_settings",
section="other_settings",
),
CONF_TRANSITION: PlatformField(
selector=BOOLEAN_SELECTOR,
@@ -3149,21 +3149,21 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
default=False,
validator=cv.boolean,
conditions=({CONF_SCHEMA: "json"},),
section="advanced_settings",
section="other_settings",
),
CONF_MAX_KELVIN: PlatformField(
selector=KELVIN_SELECTOR,
required=False,
validator=cv.positive_int,
default=DEFAULT_MAX_KELVIN,
section="advanced_settings",
section="other_settings",
),
CONF_MIN_KELVIN: PlatformField(
selector=KELVIN_SELECTOR,
required=False,
validator=cv.positive_int,
default=DEFAULT_MIN_KELVIN,
section="advanced_settings",
section="other_settings",
),
},
Platform.LOCK: {
@@ -3372,7 +3372,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
selector=TIMEOUT_SELECTOR,
required=False,
validator=cv.positive_int,
section="advanced_settings",
section="other_settings",
),
},
Platform.SIREN: {
@@ -3437,7 +3437,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
required=False,
validator=validate(cv.template),
error="invalid_template",
section="siren_advanced_settings",
section="siren_other_settings",
),
},
Platform.SWITCH: {
@@ -3516,26 +3516,26 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
selector=TEXT_SIZE_SELECTOR,
required=True,
default=0,
section="text_advanced_settings",
section="text_other_settings",
),
CONF_MAX: PlatformField(
selector=TEXT_SIZE_SELECTOR,
required=True,
default=255,
section="text_advanced_settings",
section="text_other_settings",
),
CONF_MODE: PlatformField(
selector=TEXT_MODE_SELECTOR,
required=True,
default=TextSelectorType.TEXT.value,
section="text_advanced_settings",
section="text_other_settings",
),
CONF_PATTERN: PlatformField(
selector=TEXT_SELECTOR,
required=False,
validator=validate(cv.is_regex),
error="invalid_regular_expression",
section="text_advanced_settings",
section="text_other_settings",
),
},
Platform.TIME: {
@@ -3798,10 +3798,10 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
MQTT_DEVICE_PLATFORM_FIELDS = {
CONF_NAME: PlatformField(selector=TEXT_SELECTOR, required=True),
CONF_SW_VERSION: PlatformField(
selector=TEXT_SELECTOR, required=False, section="advanced_settings"
selector=TEXT_SELECTOR, required=False, section="other_settings"
),
CONF_HW_VERSION: PlatformField(
selector=TEXT_SELECTOR, required=False, section="advanced_settings"
selector=TEXT_SELECTOR, required=False, section="other_settings"
),
CONF_MODEL: PlatformField(selector=TEXT_SELECTOR, required=False),
CONF_MODEL_ID: PlatformField(selector=TEXT_SELECTOR, required=False),
@@ -4178,7 +4178,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
CONF_PROTOCOL: DEFAULT_PROTOCOL,
CONF_USERNAME: addon_discovery_config.get(CONF_USERNAME),
CONF_PASSWORD: addon_discovery_config.get(CONF_PASSWORD),
CONF_DISCOVERY: DEFAULT_DISCOVERY,
}
except AddonError:
# We do not have discovery information yet
@@ -4419,7 +4418,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
CONF_PROTOCOL: DEFAULT_PROTOCOL,
CONF_USERNAME: data.get(CONF_USERNAME),
CONF_PASSWORD: data.get(CONF_PASSWORD),
CONF_DISCOVERY: DEFAULT_DISCOVERY,
},
)
@@ -4686,8 +4684,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
if user_input is not None:
new_device_data: dict[str, Any] = user_input.copy()
_, errors = validate_user_input(user_input, MQTT_DEVICE_PLATFORM_FIELDS)
if "advanced_settings" in new_device_data:
new_device_data |= new_device_data.pop("advanced_settings")
if "other_settings" in new_device_data:
new_device_data |= new_device_data.pop("other_settings")
if not errors:
self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, new_device_data)
if self.source == SOURCE_RECONFIGURE:
+40 -40
View File
@@ -184,17 +184,6 @@
},
"description": "Enter the MQTT device details:",
"sections": {
"advanced_settings": {
"data": {
"hw_version": "Hardware version",
"sw_version": "Software version"
},
"data_description": {
"hw_version": "The hardware version of the device. E.g. 'v1.0 rev a'.",
"sw_version": "The software version of the device. E.g. '2025.1.0'."
},
"name": "Advanced device settings"
},
"mqtt_settings": {
"data": {
"message_expiry_interval": "Message Expiry Interval",
@@ -205,6 +194,17 @@
"qos": "The Quality of Service value the device's entities should use."
},
"name": "MQTT settings"
},
"other_settings": {
"data": {
"hw_version": "Hardware version",
"sw_version": "Software version"
},
"data_description": {
"hw_version": "The hardware version of the device. E.g. 'v1.0 rev a'.",
"sw_version": "The software version of the device. E.g. '2025.1.0'."
},
"name": "Other device settings"
}
},
"title": "Configure MQTT device details"
@@ -286,14 +286,14 @@
},
"description": "Please configure specific details for {platform} entity \"{entity}\":",
"sections": {
"advanced_settings": {
"other_settings": {
"data": {
"suggested_display_precision": "Suggested display precision"
},
"data_description": {
"suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)"
},
"name": "Advanced options"
"name": "Other settings"
}
},
"title": "Configure MQTT device \"{mqtt_device}\""
@@ -438,29 +438,6 @@
},
"description": "Please configure MQTT specific details for {platform} entity \"{entity}\":",
"sections": {
"advanced_settings": {
"data": {
"expire_after": "Expire after",
"flash": "Flash support",
"flash_time_long": "Flash time long",
"flash_time_short": "Flash time short",
"max_kelvin": "Max Kelvin",
"min_kelvin": "Min Kelvin",
"off_delay": "OFF delay",
"transition": "Transition support"
},
"data_description": {
"expire_after": "If set, it defines the number of seconds after the sensors state expires, if its not updated. After expiry, the sensors state becomes unavailable. If not set, the sensor's state never expires. [Learn more.]({url}#expire_after)",
"flash": "Enable the flash feature for this light",
"flash_time_long": "The duration, in seconds, of a \"long\" flash.",
"flash_time_short": "The duration, in seconds, of a \"short\" flash.",
"max_kelvin": "The maximum color temperature in Kelvin.",
"min_kelvin": "The minimum color temperature in Kelvin.",
"off_delay": "For sensors that only send \"on\" state updates (like PIRs), this variable sets a delay in seconds after which the sensors state will be updated back to \"off\".",
"transition": "Enable the transition feature for this light"
},
"name": "Advanced settings"
},
"alarm_control_panel_payload_settings": {
"data": {
"payload_arm_away": "Payload \"arm away\"",
@@ -916,14 +893,37 @@
},
"name": "Lock payload settings"
},
"siren_advanced_settings": {
"other_settings": {
"data": {
"expire_after": "Expire after",
"flash": "Flash support",
"flash_time_long": "Flash time long",
"flash_time_short": "Flash time short",
"max_kelvin": "Max Kelvin",
"min_kelvin": "Min Kelvin",
"off_delay": "OFF delay",
"transition": "Transition support"
},
"data_description": {
"expire_after": "If set, it defines the number of seconds after the sensors state expires, if its not updated. After expiry, the sensors state becomes unavailable. If not set, the sensor's state never expires. [Learn more.]({url}#expire_after)",
"flash": "Enable the flash feature for this light",
"flash_time_long": "The duration, in seconds, of a \"long\" flash.",
"flash_time_short": "The duration, in seconds, of a \"short\" flash.",
"max_kelvin": "The maximum color temperature in Kelvin.",
"min_kelvin": "The minimum color temperature in Kelvin.",
"off_delay": "For sensors that only send \"on\" state updates (like PIRs), this variable sets a delay in seconds after which the sensors state will be updated back to \"off\".",
"transition": "Enable the transition feature for this light"
},
"name": "Other settings"
},
"siren_other_settings": {
"data": {
"command_off_template": "Command \"off\" template"
},
"data_description": {
"command_off_template": "The [template]({command_templating_url}) for \"off\" state changes. By default the \"[Command template]({url}#command_template)\" will be used. [Learn more.]({url}#command_off_template)"
},
"name": "Advanced siren settings"
"name": "Other siren settings"
},
"target_humidity_settings": {
"data": {
@@ -985,7 +985,7 @@
},
"name": "Target temperature settings"
},
"text_advanced_settings": {
"text_other_settings": {
"data": {
"max": "Maximum length",
"min": "Minimum length",
@@ -998,7 +998,7 @@
"mode": "Mode of the text input",
"pattern": "A valid regex pattern"
},
"name": "Advanced text entity settings"
"name": "Other text entity settings"
},
"valve_payload_settings": {
"data": {
@@ -1,7 +1,7 @@
{
"domain": "myneomitis",
"name": "MyNeomitis",
"codeowners": ["@l-pr"],
"codeowners": ["@Epyes"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/myneomitis",
"integration_type": "hub",
@@ -70,5 +70,8 @@ class MyUplinkDataCoordinator(DataUpdateCoordinator[CoordinatorData]):
points[device_id] = point_info
return CoordinatorData(
systems=systems, devices=devices, points=points, time=datetime.now()
systems=systems,
devices=devices,
points=points,
time=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now
)
@@ -67,7 +67,7 @@ class NextBusDataUpdateCoordinator(
# But only if we have a reset time to unthrottle
and self.client.rate_limit_reset is not None
# Unless we are after the reset time
and datetime.now() < self.client.rate_limit_reset
and datetime.now() < self.client.rate_limit_reset # pylint: disable=home-assistant-enforce-naive-now
):
self.logger.debug(
"Rate limit threshold reached. Skipping updates for. Routes: %s",
@@ -58,7 +58,7 @@ class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_EMAIL: user_input[CONF_EMAIL],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
},
)
@@ -99,7 +99,7 @@ class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
data={
**user_input,
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
},
unique_id=user_input[CONF_EMAIL],
)
@@ -150,7 +150,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
+ REFRESH_TOKEN_EXPIRY_TIME.total_seconds()
)
try:
if datetime.now().timestamp() >= expiry_time:
if datetime.now().timestamp() >= expiry_time: # pylint: disable=home-assistant-enforce-naive-now
await self.update_refresh_token()
else:
await self.api.authenticate_refresh(
@@ -194,7 +194,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
data = {
**self.config_entry.data,
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
}
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
@@ -154,7 +154,7 @@ class NOAATidesAndCurrentsSensor(SensorEntity):
def update(self) -> None:
"""Get the latest data from NOAA Tides and Currents API."""
begin = datetime.now()
begin = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
end = begin + DEFAULT_PREDICTION_LENGTH
try:
df_predictions = self._station.get_data(
@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "platinum",
"requirements": ["opower==0.18.4"]
"requirements": ["opower==0.18.5"]
}
+1 -1
View File
@@ -385,7 +385,7 @@ class DailyHistory:
def add_measurement(self, value, timestamp=None):
"""Add a new measurement for a certain day."""
day = (timestamp or datetime.now()).date()
day = (timestamp or datetime.now()).date() # pylint: disable=home-assistant-enforce-naive-now
if not isinstance(value, (int, float)):
return
if self._days is None:
@@ -26,6 +26,8 @@ from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
from .helpers import is_granted
PARALLEL_UPDATES = 1
NO_PERM_VM_LXC_POWER = "no_permission_vm_lxc_power"
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
from .coordinator import RabbitAirConfigEntry, RabbitAirDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.FAN]
PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: RabbitAirConfigEntry) -> bool:
+2 -1
View File
@@ -25,6 +25,8 @@ MODELS = {
class RabbitAirBaseEntity(CoordinatorEntity[RabbitAirDataUpdateCoordinator]):
"""Base class for Rabbit Air entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: RabbitAirDataUpdateCoordinator,
@@ -32,7 +34,6 @@ class RabbitAirBaseEntity(CoordinatorEntity[RabbitAirDataUpdateCoordinator]):
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_name = entry.title
self._attr_unique_id = entry.unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.data[CONF_MAC])},
@@ -46,6 +46,7 @@ async def async_setup_entry(
class RabbitAirFanEntity(RabbitAirBaseEntity, FanEntity):
"""Fan control functions of the Rabbit Air air purifier."""
_attr_name = None
_attr_translation_key = "rabbitair"
_attr_supported_features = (
FanEntityFeature.PRESET_MODE
@@ -12,6 +12,11 @@
}
}
}
},
"sensor": {
"air_quality": {
"default": "mdi:air-filter"
}
}
}
}
@@ -0,0 +1,60 @@
"""Support for Rabbit Air sensors."""
from rabbitair import Quality
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .coordinator import RabbitAirConfigEntry, RabbitAirDataUpdateCoordinator
from .entity import RabbitAirBaseEntity
def _quality_value(quality: Quality | None) -> StateType:
"""Return the air quality state."""
return None if quality is None else quality.name.lower()
AIR_QUALITY_OPTIONS = [quality.name.lower() for quality in Quality]
AIR_QUALITY_DESCRIPTION = SensorEntityDescription(
key="air_quality",
translation_key="air_quality",
device_class=SensorDeviceClass.ENUM,
options=AIR_QUALITY_OPTIONS,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: RabbitAirConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Rabbit Air sensors."""
if entry.runtime_data.data.quality is not None:
async_add_entities([RabbitAirAirQualitySensor(entry.runtime_data, entry)])
class RabbitAirAirQualitySensor(RabbitAirBaseEntity, SensorEntity):
"""Rabbit Air air quality sensor."""
entity_description = AIR_QUALITY_DESCRIPTION
def __init__(
self,
coordinator: RabbitAirDataUpdateCoordinator,
entry: RabbitAirConfigEntry,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator, entry)
self._attr_unique_id = f"{entry.unique_id}_{self.entity_description.key}"
@property
def native_value(self) -> StateType:
"""Return the air quality state."""
return _quality_value(self.coordinator.data.quality)
@@ -32,6 +32,18 @@
}
}
}
},
"sensor": {
"air_quality": {
"name": "Air quality",
"state": {
"high": "[%key:common::state::high%]",
"highest": "Highest",
"low": "[%key:common::state::low%]",
"lowest": "Lowest",
"medium": "[%key:common::state::medium%]"
}
}
}
}
}
@@ -184,7 +184,7 @@ class CalendarUpdateCoordinator(RadarrDataUpdateCoordinator[None]):
self._events = [
e
for e in self._events
if e.start >= datetime.now().date() - timedelta(days=30)
if e.start >= datetime.now().date() - timedelta(days=30) # pylint: disable=home-assistant-enforce-naive-now
]
_days = (end_date - start_date).days
await asyncio.gather(
@@ -19,7 +19,7 @@ async def async_get_config_entry_diagnostics(
payload: dict[str, Any] = {
"now": dt_util.now().isoformat(),
"timezone": str(dt_util.get_default_time_zone()),
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo),
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo), # pylint: disable=home-assistant-enforce-naive-now
}
payload["ics"] = "\n".join(redact_ics(coordinator.ics))
return payload
+1 -1
View File
@@ -31,7 +31,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
time_to_send = time
if time is None:
time_to_send = datetime.now()
time_to_send = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
await local_data.system.set_time(time_to_send)
@@ -46,7 +46,7 @@ class SensoterraConfigFlow(ConfigFlow, domain=DOMAIN):
api = CustomerApi(user_input[CONF_EMAIL], user_input[CONF_PASSWORD])
# We need a unique tag per HA instance
uuid = self.hass.data["core.uuid"]
expiration = datetime.now() + timedelta(TOKEN_EXPIRATION_DAYS)
expiration = datetime.now() + timedelta(TOKEN_EXPIRATION_DAYS) # pylint: disable=home-assistant-enforce-naive-now
try:
token: str = await api.get_token(
@@ -70,7 +70,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator[bool]):
try:
if (
self.ayla_api.token_expiring_soon
or datetime.now()
or datetime.now() # pylint: disable=home-assistant-enforce-naive-now
> self.ayla_api.auth_expiration - timedelta(seconds=600)
):
await self.ayla_api.async_refresh_auth()
@@ -247,7 +247,7 @@ def _async_register_base_station(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, str(system.system_id))},
manufacturer="SimpliSafe",
model=system.version,
model=str(system.version),
name=system.address,
)
+2 -2
View File
@@ -17,7 +17,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import discovery
from homeassistant.util.ssl import create_client_context
@@ -75,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmtpConfigEntry) -> bool
try:
await hass.async_add_executor_job(lambda: client.connect().quit())
except SMTPAuthenticationError as e:
raise ConfigEntryError(
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentication_error",
) from e
@@ -1,5 +1,6 @@
"""Config flow for the SMTP integration."""
from collections.abc import Mapping
import logging
from smtplib import SMTP, SMTP_SSL, SMTPAuthenticationError
import socket
@@ -95,6 +96,22 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_VERIFY_SSL, default=True): cv.boolean,
}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_USERNAME): TextSelector(
TextSelectorConfig(
type=TextSelectorType.TEXT,
autocomplete="username",
),
),
vol.Optional(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
),
),
}
)
OPTIONS_SCHEMA = vol.Schema(
{
@@ -201,6 +218,39 @@ class MailConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication dialog."""
errors: dict[str, str] = {}
entry = self._get_reauth_entry()
if user_input is not None:
errors = await self.hass.async_add_executor_job(
validate_input, {**entry.data, **user_input}
)
if not errors:
return self.async_update_and_abort(
entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
data_schema=STEP_REAUTH_DATA_SCHEMA,
suggested_values=user_input
or {CONF_USERNAME: entry.data.get(CONF_USERNAME)},
),
errors=errors,
)
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
"""Import config from yaml."""
+2 -2
View File
@@ -41,7 +41,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -208,7 +208,7 @@ class MailNotifyEntity(NotifyEntity):
try:
client = self._client.connect()
except SMTPAuthenticationError as e:
raise HomeAssistantError(
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentication_error",
) from e
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
@@ -11,6 +12,17 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"password": "[%key:component::smtp::config::step::user::data_description::password%]",
"username": "[%key:component::smtp::config::step::user::data_description::username%]"
},
"title": "Re-authenticate SMTP"
},
"reconfigure": {
"data": {
"encryption": "[%key:component::smtp::config::step::user::data::encryption%]",
+1 -1
View File
@@ -47,7 +47,7 @@ class StarlineAccount:
def _check_slnet_token(self, interval: int) -> None:
"""Check SLNet token expiration and update if needed."""
now = datetime.now().timestamp()
now = datetime.now().timestamp() # pylint: disable=home-assistant-enforce-naive-now
slnet_token_expires = self._config_entry.data[DATA_EXPIRES]
if now + interval > slnet_token_expires:
@@ -40,11 +40,6 @@ class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity):
"""Return device specific attributes."""
return self._account.gps_attrs(self._device)
@property
def battery_level(self) -> int | None:
"""Return the battery level of the device."""
return self._device.battery_level
@property
def location_accuracy(self) -> float:
"""Return the gps accuracy of the device."""
@@ -108,7 +108,7 @@ class SubaruConfigFlow(ConfigFlow, domain=DOMAIN):
data: contains values provided by the user.
"""
websession = aiohttp_client.async_get_clientsession(self.hass)
now = datetime.now()
now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
if not data.get(CONF_DEVICE_ID):
data[CONF_DEVICE_ID] = int(now.timestamp())
date = now.strftime("%Y-%m-%d")
+8 -1
View File
@@ -5,7 +5,7 @@ import logging
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
@@ -50,6 +50,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool:
"""Set up from a config entry."""
# Remove deprecated solar_rising sensor entity (removed in 2026.1)
ent_reg = er.async_get(hass)
if entity_id := ent_reg.async_get_entity_id(
Platform.SENSOR, DOMAIN, f"{entry.entry_id}-solar_rising"
):
ent_reg.async_remove(entity_id)
sun = Sun(hass)
component = EntityComponent[Sun](_LOGGER, DOMAIN, hass)
await component.async_add_entities([sun])
@@ -57,7 +57,7 @@ class SwitchBotCloudImage(SwitchBotCloudEntity, ImageEntity):
"""Set attributes from coordinator data."""
if self.coordinator.data is None:
return
self._attr_image_last_updated = datetime.datetime.now()
self._attr_image_last_updated = datetime.datetime.now() # pylint: disable=home-assistant-enforce-naive-now
self._attr_image_url = self.coordinator.data.get("imageUrl")
+1 -1
View File
@@ -447,7 +447,7 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def set_meter_reading(self, reading: int) -> dict[str, Any]:
"""Send meter reading to Tado."""
dt: str = datetime.now().strftime("%Y-%m-%d")
dt: str = datetime.now().strftime("%Y-%m-%d") # pylint: disable=home-assistant-enforce-naive-now
if self._tado is None:
raise HomeAssistantError("Tado client is not initialized")
+16 -10
View File
@@ -1,15 +1,12 @@
"""Support for Template fans."""
from enum import StrEnum
import logging
from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components.fan import (
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
DIRECTION_FORWARD,
DIRECTION_REVERSE,
DOMAIN as FAN_DOMAIN,
@@ -100,6 +97,15 @@ FAN_CONFIG_ENTRY_SCHEMA = FAN_COMMON_SCHEMA.extend(
)
class FanScriptVariable(StrEnum):
"""Variables for scripts."""
DIRECTION = "direction"
OSCILLATING = "oscillating"
PERCENTAGE = "percentage"
PRESET_MODE = "preset_mode"
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
@@ -235,8 +241,8 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
await self.async_run_script(
self._action_scripts[CONF_ON_ACTION],
run_variables={
ATTR_PERCENTAGE: percentage,
ATTR_PRESET_MODE: preset_mode,
FanScriptVariable.PERCENTAGE: percentage,
FanScriptVariable.PRESET_MODE: preset_mode,
},
context=self._context,
)
@@ -267,7 +273,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
if script := self._action_scripts.get(CONF_SET_PERCENTAGE_ACTION):
await self.async_run_script(
script,
run_variables={ATTR_PERCENTAGE: self._attr_percentage},
run_variables={FanScriptVariable.PERCENTAGE: self._attr_percentage},
context=self._context,
)
@@ -284,7 +290,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
if script := self._action_scripts.get(CONF_SET_PRESET_MODE_ACTION):
await self.async_run_script(
script,
run_variables={ATTR_PRESET_MODE: self._attr_preset_mode},
run_variables={FanScriptVariable.PRESET_MODE: self._attr_preset_mode},
context=self._context,
)
@@ -302,7 +308,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
) is not None:
await self.async_run_script(
script,
run_variables={ATTR_OSCILLATING: self.oscillating},
run_variables={FanScriptVariable.OSCILLATING: self.oscillating},
context=self._context,
)
@@ -318,7 +324,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
) is not None:
await self.async_run_script(
script,
run_variables={ATTR_DIRECTION: direction},
run_variables={FanScriptVariable.DIRECTION: direction},
context=self._context,
)
if CONF_DIRECTION not in self._templates:
+1 -2
View File
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components.number import (
ATTR_VALUE,
DEFAULT_MAX_VALUE,
DEFAULT_MIN_VALUE,
DEFAULT_STEP,
@@ -161,7 +160,7 @@ class AbstractTemplateNumber(AbstractTemplateEntity, NumberEntity):
if set_value := self._action_scripts.get(CONF_SET_VALUE):
await self.async_run_script(
set_value,
run_variables={ATTR_VALUE: value},
run_variables={"value": value},
context=self._context,
)
+3 -5
View File
@@ -6,8 +6,6 @@ from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components.select import (
ATTR_OPTION,
ATTR_OPTIONS,
DOMAIN as SELECT_DOMAIN,
ENTITY_ID_FORMAT,
SelectEntity,
@@ -48,7 +46,7 @@ SCRIPT_FIELDS = (CONF_SELECT_OPTION,)
SELECT_COMMON_SCHEMA = vol.Schema(
{
vol.Required(ATTR_OPTIONS): cv.template,
vol.Required(CONF_OPTIONS): cv.template,
vol.Optional(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_STATE): cv.template,
}
@@ -147,7 +145,7 @@ class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity):
if select_option := self._action_scripts.get(CONF_SELECT_OPTION):
await self.async_run_script(
select_option,
run_variables={ATTR_OPTION: option},
run_variables={"option": option},
context=self._context,
)
@@ -175,7 +173,7 @@ class TriggerSelectEntity(TriggerEntity, AbstractTemplateSelect):
"""Select entity based on trigger data."""
domain = SELECT_DOMAIN
extra_template_keys_complex = (ATTR_OPTIONS,)
extra_template_keys_complex = (CONF_OPTIONS,)
def __init__(
self,
+5 -5
View File
@@ -9,7 +9,6 @@ from typing import Any
import voluptuous as vol
from homeassistant.components.sensor import (
ATTR_LAST_RESET,
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
@@ -50,13 +49,14 @@ from .schemas import (
from .template_entity import TemplateEntity
from .trigger_entity import TriggerEntity
CONF_LAST_RESET = "last_reset"
DEFAULT_NAME = "Template Sensor"
def validate_last_reset(val):
"""Run extra validation checks."""
if (
val.get(ATTR_LAST_RESET) is not None
val.get(CONF_LAST_RESET) is not None
and val.get(CONF_STATE_CLASS) != SensorStateClass.TOTAL
):
raise vol.Invalid(
@@ -78,7 +78,7 @@ SENSOR_COMMON_SCHEMA = vol.Schema(
SENSOR_YAML_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(ATTR_LAST_RESET): cv.template,
vol.Optional(CONF_LAST_RESET): cv.template,
}
)
.extend(SENSOR_COMMON_SCHEMA.schema)
@@ -204,10 +204,10 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
self._validate_state,
)
self.setup_template(
ATTR_LAST_RESET,
CONF_LAST_RESET,
"_attr_last_reset",
validate_datetime(
self, ATTR_LAST_RESET, SensorDeviceClass.TIMESTAMP, require_tzinfo=False
self, CONF_LAST_RESET, SensorDeviceClass.TIMESTAMP, require_tzinfo=False
),
)
+1 -2
View File
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components.vacuum import (
ATTR_FAN_SPEED,
DOMAIN as VACUUM_DOMAIN,
SERVICE_CLEAN_SPOT,
SERVICE_LOCATE,
@@ -389,7 +388,7 @@ class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
if script := self._action_scripts.get(SERVICE_SET_FAN_SPEED):
await self.async_run_script(
script, run_variables={ATTR_FAN_SPEED: fan_speed}, context=self._context
script, run_variables={"fan_speed": fan_speed}, context=self._context
)
@@ -109,7 +109,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self.api = api
self.data = flatten(product)
self.updated_once = False
self.last_active = datetime.now()
self.last_active = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
self.endpoints = (
ENDPOINTS
if location
@@ -159,12 +159,12 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
or data["vehicle_state"].get("sentry_mode")
):
# Vehicle is active, reset timer
self.last_active = datetime.now()
self.last_active = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
else:
elapsed = datetime.now() - self.last_active
elapsed = datetime.now() - self.last_active # pylint: disable=home-assistant-enforce-naive-now
if elapsed > timedelta(minutes=20):
# Vehicle didn't sleep, try again in 15 minutes
self.last_active = datetime.now()
self.last_active = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
elif elapsed > timedelta(minutes=15):
# Let vehicle go to sleep now
self.update_interval = VEHICLE_WAIT
@@ -129,7 +129,7 @@ class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self.api = api
self.data = flatten(product)
self.last_active = datetime.now()
self.last_active = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
async def _async_update_data(self) -> dict[str, Any]:
"""Update vehicle data using Teslemetry API."""
+1 -1
View File
@@ -232,7 +232,7 @@ class TwitterNotificationService(BaseNotificationService):
"media processing waiting %s seconds to check status", str(check_after_secs)
)
when = datetime.now() + timedelta(seconds=check_after_secs)
when = datetime.now() + timedelta(seconds=check_after_secs) # pylint: disable=home-assistant-enforce-naive-now
myself = partial(self.check_status_until_done, media_id, callback)
async_track_point_in_time(self.hass, myself, when)
@@ -42,7 +42,7 @@ class VeSyncDataCoordinator(DataUpdateCoordinator[None]):
if self.update_time is None:
return True
return datetime.now() - self.update_time >= timedelta(
return datetime.now() - self.update_time >= timedelta( # pylint: disable=home-assistant-enforce-naive-now
seconds=UPDATE_INTERVAL_ENERGY
)
@@ -52,7 +52,7 @@ class VeSyncDataCoordinator(DataUpdateCoordinator[None]):
await self.manager.update_all_devices()
if self.should_update_energy():
self.update_time = datetime.now()
self.update_time = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
for outlet in self.manager.devices.outlets:
await outlet.update_energy()
except VeSyncError as err:
@@ -101,7 +101,7 @@ def _require_authentication[_WallboxCoordinatorT: WallboxCoordinator, **_P](
def check_token_validity(jwt_token_ttl: int, jwt_token_drift: int) -> bool:
"""Check if the jwtToken is still valid in order to reuse if possible."""
return round((jwt_token_ttl / 1000) - jwt_token_drift, 0) > datetime.timestamp(
datetime.now()
datetime.now() # pylint: disable=home-assistant-enforce-naive-now
)
@@ -66,7 +66,7 @@ class WattsVisionHubCoordinator(DataUpdateCoordinator[dict[str, Device]]):
async def _async_update_data(self) -> dict[str, Device]:
"""Fetch data and periodic device discovery."""
now = datetime.now()
now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
is_first_refresh = self.last_discovery is None
discovery_interval_elapsed = (
self.last_discovery is not None
@@ -204,7 +204,7 @@ class WattsVisionDeviceCoordinator(DataUpdateCoordinator[WattsVisionDeviceData])
async def _async_update_data(self) -> WattsVisionDeviceData:
"""Refresh specific device."""
if self.fast_polling_until and datetime.now() > self.fast_polling_until:
if self.fast_polling_until and datetime.now() > self.fast_polling_until: # pylint: disable=home-assistant-enforce-naive-now
self.fast_polling_until = None
self.update_interval = None
_LOGGER.debug(
@@ -242,7 +242,7 @@ class WattsVisionDeviceCoordinator(DataUpdateCoordinator[WattsVisionDeviceData])
def trigger_fast_polling(self, duration: int = 60) -> None:
"""Activate fast polling for a specified duration after a command."""
self.fast_polling_until = datetime.now() + timedelta(seconds=duration)
self.fast_polling_until = datetime.now() + timedelta(seconds=duration) # pylint: disable=home-assistant-enforce-naive-now
self.update_interval = timedelta(seconds=FAST_POLLING_INTERVAL_SECONDS)
_LOGGER.debug(
"Device %s: Activated fast polling for %d seconds", self.device_id, duration
@@ -21,7 +21,7 @@ async def async_get_config_entry_diagnostics(
runtime_data = entry.runtime_data
hub_coordinator = runtime_data.hub_coordinator
device_coordinators = runtime_data.device_coordinators
now = datetime.now()
now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
return async_redact_data(
{
@@ -81,12 +81,12 @@ class WeatherKitDataUpdateCoordinator(DataUpdateCoordinator):
except WeatherKitApiClientError as exception:
if self.data is None or (
self.last_updated_at is not None
and datetime.now() - self.last_updated_at > STALE_DATA_THRESHOLD
and datetime.now() - self.last_updated_at > STALE_DATA_THRESHOLD # pylint: disable=home-assistant-enforce-naive-now
):
raise UpdateFailed(exception) from exception
LOGGER.debug("Using stale data because update failed: %s", exception)
return self.data
else:
self.last_updated_at = datetime.now()
self.last_updated_at = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
return updated_data
+24 -1
View File
@@ -1,5 +1,8 @@
"""The WMS WebControl pro API integration."""
from pathlib import Path
import shutil
import aiohttp
from wmspro.webcontrol import WebControlPro
@@ -9,6 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import UNDEFINED
from .const import DOMAIN, MANUFACTURER
@@ -17,6 +21,7 @@ PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.COVER,
Platform.LIGHT,
Platform.NUMBER,
Platform.SCENE,
Platform.SWITCH,
]
@@ -24,13 +29,22 @@ PLATFORMS: list[Platform] = [
type WebControlProConfigEntry = ConfigEntry[WebControlPro]
def _build_storage_config_dir(
hass: HomeAssistant, entry: WebControlProConfigEntry
) -> Path:
"""Build the path to the config directory for a given config entry."""
return Path(hass.config.path(STORAGE_DIR, f"{DOMAIN}-{entry.entry_id}"))
async def async_setup_entry(
hass: HomeAssistant, entry: WebControlProConfigEntry
) -> bool:
"""Set up wmspro from a config entry."""
host = entry.data[CONF_HOST]
session = async_get_clientsession(hass)
hub = WebControlPro(host, session)
config_dir = _build_storage_config_dir(hass, entry)
config_dir.mkdir(parents=True, exist_ok=True)
hub = WebControlPro(host, session, str(config_dir))
try:
await hub.ping()
@@ -68,3 +82,12 @@ async def async_unload_entry(
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_remove_entry(
hass: HomeAssistant, entry: WebControlProConfigEntry
) -> None:
"""Remove a config entry."""
config_dir = _build_storage_config_dir(hass, entry)
if config_dir.is_dir():
await hass.async_add_executor_job(shutil.rmtree, config_dir)
+23 -6
View File
@@ -3,6 +3,7 @@
from wmspro.const import WMS_WebControl_pro_API_actionDescription
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -18,11 +19,12 @@ async def async_setup_entry(
"""Set up the WMS based identify buttons from a config entry."""
hub = config_entry.runtime_data
entities: list[WebControlProGenericEntity] = [
WebControlProIdentifyButton(config_entry.entry_id, dest)
for dest in hub.dests.values()
if dest.hasAction(WMS_WebControl_pro_API_actionDescription.Identify)
]
entities: list[WebControlProGenericEntity] = []
for d in hub.dests.values():
if d.hasAction(WMS_WebControl_pro_API_actionDescription.Identify):
entities.append(WebControlProIdentifyButton(config_entry.entry_id, d))
if d.hasAction(WMS_WebControl_pro_API_actionDescription.SlatRotate):
entities.append(WebControlProRotationResetButton(config_entry.entry_id, d))
async_add_entities(entities)
@@ -33,6 +35,21 @@ class WebControlProIdentifyButton(WebControlProGenericEntity, ButtonEntity):
_attr_device_class = ButtonDeviceClass.IDENTIFY
async def async_press(self) -> None:
"""Handle the button press."""
"""Handle the button press to identify the device."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.Identify)
await action()
class WebControlProRotationResetButton(WebControlProGenericEntity, ButtonEntity):
"""Representation of a WMS based reset rotation button."""
_attr_entity_category = EntityCategory.CONFIG
_attr_translation_key = "rotation-reset"
async def async_press(self) -> None:
"""Handle the button press to reset the rotation range to the default."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.SlatRotate)
# Delete the min and max override values to reset the rotation range to the default
del action["minValue"]
del action["maxValue"]
# The library will take care of the update and persistence on the next poll refresh
+100 -17
View File
@@ -8,11 +8,19 @@ from wmspro.const import (
WMS_WebControl_pro_API_actionType,
WMS_WebControl_pro_API_responseType,
)
from wmspro.destination import Destination
from homeassistant.components.cover import ATTR_POSITION, CoverDeviceClass, CoverEntity
from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
CoverDeviceClass,
CoverEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.percentage import (
percentage_to_ranged_value,
ranged_value_to_percentage,
)
from . import WebControlProConfigEntry
from .entity import WebControlProGenericEntity
@@ -30,13 +38,18 @@ async def async_setup_entry(
hub = config_entry.runtime_data
entities: list[WebControlProGenericEntity] = []
for dest in hub.dests.values():
if dest.hasAction(ACTION_DESC.AwningDrive):
entities.append(WebControlProAwning(config_entry.entry_id, dest))
if dest.hasAction(ACTION_DESC.ValanceDrive):
entities.append(WebControlProValance(config_entry.entry_id, dest))
if dest.hasAction(ACTION_DESC.RollerShutterBlindDrive):
entities.append(WebControlProRollerShutter(config_entry.entry_id, dest))
for d in hub.dests.values():
if d.hasAction(ACTION_DESC.AwningDrive):
entities.append(WebControlProAwning(config_entry.entry_id, d))
if d.hasAction(ACTION_DESC.ValanceDrive):
entities.append(WebControlProValance(config_entry.entry_id, d))
elif d.hasAction(ACTION_DESC.RollerShutterBlindDrive):
entities.append(WebControlProRollerShutter(config_entry.entry_id, d))
elif d.hasAction(ACTION_DESC.SlatDrive):
if d.hasAction(ACTION_DESC.SlatRotate):
entities.append(WebControlProSlatRotate(config_entry.entry_id, d))
else:
entities.append(WebControlProSlat(config_entry.entry_id, d))
async_add_entities(entities)
@@ -51,7 +64,7 @@ class WebControlProCover(WebControlProGenericEntity, CoverEntity):
def current_cover_position(self) -> int | None:
"""Return current position of cover."""
action = self._dest.action(self._drive_action_desc)
if action is None or action["percentage"] is None:
if action["percentage"] is None:
return None
return 100 - action["percentage"]
@@ -94,19 +107,89 @@ class WebControlProAwning(WebControlProCover):
class WebControlProValance(WebControlProCover):
"""Representation of a WMS based valance."""
_attr_translation_key = "valance"
_attr_device_class = CoverDeviceClass.SHADE
_attr_translation_key = "valance"
_drive_action_desc = ACTION_DESC.ValanceDrive
def __init__(self, config_entry_id: str, dest: Destination) -> None:
"""Initialize the entity with destination channel."""
super().__init__(config_entry_id, dest)
if self._attr_unique_id:
self._attr_unique_id += "-valance"
class WebControlProRollerShutter(WebControlProCover):
"""Representation of a WMS based roller shutter or blind."""
_attr_device_class = CoverDeviceClass.SHUTTER
_drive_action_desc = ACTION_DESC.RollerShutterBlindDrive
class WebControlProSlat(WebControlProCover):
"""Representation of a WMS based blind using a slat drive."""
_attr_device_class = CoverDeviceClass.BLIND
_drive_action_desc = ACTION_DESC.SlatDrive
class WebControlProSlatRotate(WebControlProSlat):
"""Representation of a WMS based blind which supports tilting."""
_tilt_action_desc = ACTION_DESC.SlatRotate
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover and tilt to minimum like the WMS WebControl pro."""
action_drive = self._dest.action(self._drive_action_desc)
action_list = action_drive.prep(percentage=0)
action_tilt = self._dest.action(self._tilt_action_desc)
action_list += action_tilt.prep(rotation=action_tilt.minValue)
await action_list()
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover and tilt to maximum like the WMS WebControl pro."""
action_drive = self._dest.action(self._drive_action_desc)
action_list = action_drive.prep(percentage=100)
action_tilt = self._dest.action(self._tilt_action_desc)
action_list += action_tilt.prep(rotation=action_tilt.maxValue)
await action_list()
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position and tilt for open/close."""
target_position = kwargs[ATTR_POSITION]
if target_position == 0:
await self.async_close_cover()
elif target_position == 100:
await self.async_open_cover()
else:
await super().async_set_cover_position(**kwargs)
@property
def current_cover_tilt_position(self) -> int | None:
"""Return current position of cover tilt."""
action = self._dest.action(self._tilt_action_desc)
if action["rotation"] is None:
return None
return 100 - ranged_value_to_percentage(
(action.minValue, action.maxValue),
action["rotation"],
)
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Set the cover tilt position."""
action = self._dest.action(self._tilt_action_desc)
rotation = percentage_to_ranged_value(
(action.minValue, action.maxValue),
100 - kwargs[ATTR_TILT_POSITION],
)
await action(rotation=rotation)
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
action = self._dest.action(self._tilt_action_desc)
# 0 is the open position for the tilt in WMS WebControl pro,
# with the open position the slat is parallel to the ground.
# This position will let the most light through and
# is required to fully store the cover in the box.
await action(rotation=0)
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
action = self._dest.action(self._tilt_action_desc)
# maxValue is the close position for the tilt in WMS WebControl pro,
# with the close position the slat is perpendicular to the ground.
# This position will block the light best.
await action(rotation=action.maxValue)
@@ -19,6 +19,8 @@ class WebControlProGenericEntity(Entity):
dest_id_str = str(dest.id)
self._dest = dest
self._attr_unique_id = dest_id_str
if self.translation_key:
self._attr_unique_id += f"-{self.translation_key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, dest_id_str)},
manufacturer=MANUFACTURER,
@@ -0,0 +1,20 @@
{
"entity": {
"button": {
"rotation-reset": {
"default": "mdi:numeric-0-box-multiple-outline"
}
},
"number": {
"rotation": {
"default": "mdi:rotate-360"
},
"rotation-max": {
"default": "mdi:rotate-right"
},
"rotation-min": {
"default": "mdi:rotate-left"
}
}
}
}
+160
View File
@@ -0,0 +1,160 @@
"""Support for range-options of slat-cover connected with WMS WebControl pro."""
from collections.abc import Callable
from datetime import timedelta
from wmspro.const import WMS_WebControl_pro_API_actionDescription as ACTION_DESC
from homeassistant.components.number import NumberEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import WebControlProConfigEntry
from .entity import WebControlProGenericEntity
SCAN_INTERVAL = timedelta(seconds=15)
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: WebControlProConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the WMS based slat rotation number entities from a config entry."""
hub = config_entry.runtime_data
entities: list[WebControlProGenericEntity] = []
for d in hub.dests.values():
if d.hasAction(ACTION_DESC.SlatRotate):
entities.append(WebControlProSlatRangeMin(config_entry.entry_id, d))
entities.append(WebControlProSlatRangeMax(config_entry.entry_id, d))
entities.append(WebControlProSlatRotationRaw(config_entry.entry_id, d))
if not d.hasAction(ACTION_DESC.SlatDrive):
# Only add the numeric slat rotation entity if no cover entity exists
entities.append(WebControlProSlatRotation(config_entry.entry_id, d))
async_add_entities(entities)
class WebControlProSlatRange(WebControlProGenericEntity, NumberEntity):
"""Representation of a WMS based range-option for a slat-based cover."""
_attr_entity_category = EntityCategory.CONFIG
_value_func: Callable
_value_name: str
async def async_update(self) -> None:
"""Update the entity and learn current rotation."""
await super().async_update()
# Learn min/max rotation if different from action limits
action = self._dest.action(ACTION_DESC.SlatRotate)
rotation = action["rotation"]
if rotation and action.wms__minValue < rotation < action.wms__maxValue:
await self.async_set_native_value(
self._value_func(self.native_value, rotation)
)
@property
def native_min_value(self) -> float:
"""Return the minimum value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
# Use wms__ prefix to get the raw value from the hub without overwrite
return self._value_func(action.wms__minValue, 0)
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
# Use wms__ prefix to get the raw value from the hub without overwrite
return self._value_func(0, action.wms__maxValue)
@property
def native_value(self) -> float:
"""Return the current min/max value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
# Pull the current min/max rotation from the custom overwrite if set
value = action[self._value_name]
# -75 and 75 are community-provided sane defaults for various devices
if value is None:
value = self._value_func(-75, 75)
return value
async def async_set_native_value(self, value: float) -> None:
"""Update the current min/max value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
# Push the new min/max rotation to the hub as custom overwrite via the action items
action[self._value_name] = value
# The library will take care of the update and persistence on the next poll refresh
class WebControlProSlatRangeMin(WebControlProSlatRange):
"""Representation of the minimum rotation range-option for a slat-based cover."""
_attr_translation_key = "rotation-min"
_value_name = "minValue"
_value_func = min
class WebControlProSlatRangeMax(WebControlProSlatRange):
"""Representation of the maximum rotation range-option for a slat-based cover."""
_attr_translation_key = "rotation-max"
_value_name = "maxValue"
_value_func = max
class WebControlProSlatRotation(WebControlProGenericEntity, NumberEntity):
"""Representation of the WMS based slat-rotation for a slat-based cover."""
_attr_translation_key = "rotation"
@property
def native_min_value(self) -> float:
"""Return the minimum value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
return action.minValue
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
return action.maxValue
@property
def native_value(self) -> float | None:
"""Return the current value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
rotation = action["rotation"]
if rotation is None:
return None
return rotation
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
await action(rotation=value)
class WebControlProSlatRotationRaw(WebControlProSlatRotation):
"""Representation of the WMS based raw slat-rotation for a slat-based cover."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_translation_key = "rotation-raw"
@property
def native_min_value(self) -> float:
"""Return the minimum value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
return action.wms__minValue
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
action = self._dest.action(ACTION_DESC.SlatRotate)
return action.wms__maxValue
@@ -21,5 +21,31 @@
}
}
}
},
"entity": {
"button": {
"rotation-reset": {
"name": "Reset Rotation"
}
},
"cover": {
"valance": {
"name": "Valance"
}
},
"number": {
"rotation": {
"name": "Rotation"
},
"rotation-max": {
"name": "Maximum Rotation"
},
"rotation-min": {
"name": "Minimum Rotation"
},
"rotation-raw": {
"name": "Raw Rotation"
}
}
}
}
@@ -139,11 +139,11 @@ class XiaomiCoordinatedMiioEntity[_T: DataUpdateCoordinator[Any]](
@staticmethod
def _parse_datetime_time(initial_time: datetime.time) -> str:
time = datetime.datetime.now().replace(
time = datetime.datetime.now().replace( # pylint: disable=home-assistant-enforce-naive-now
hour=initial_time.hour, minute=initial_time.minute, second=0, microsecond=0
)
if time < datetime.datetime.now():
if time < datetime.datetime.now(): # pylint: disable=home-assistant-enforce-naive-now
time += datetime.timedelta(days=1)
return time.isoformat()
+1 -1
View File
@@ -61,7 +61,7 @@ def _activity_time_based(latest: Activity) -> Activity | None:
"""Get the latest state of the sensor."""
start = latest.activity_start_time
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
if start <= datetime.now() <= end:
if start <= datetime.now() <= end: # pylint: disable=home-assistant-enforce-naive-now
return latest
return None
@@ -5,7 +5,7 @@ rules:
comment: This integration does not register custom service actions.
appropriate-polling:
status: done
comment: 5 minute interval. MQTT carries live state; polling is what surfaces the online -> offline transition since the broker doesn't push disconnect events.
comment: Live state is pushed over MQTT; the coordinator polls REST every 5 min only for the device roster and player config (firmware, day/night times).
brands: done
common-modules: done
config-flow-test-coverage: done
@@ -48,7 +48,7 @@ rules:
diagnostics: done
discovery-update-info:
status: exempt
comment: The integration supports local DHCP discovery (via hostname pattern), but does not implement a separate discovery update handling flow.
comment: Cloud connection; DHCP discovery only triggers setup, no network address is persisted to update.
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
@@ -70,7 +70,7 @@ rules:
comment: Authorization is the only configuration; reauth covers re-linking the account.
repair-issues:
status: exempt
comment: No repair issues are raised yet.
comment: Auth failures go through the reauth flow; other errors are transient and retried by the coordinator.
stale-devices: todo
# Platinum
@@ -1624,7 +1624,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
# save the backup to a file just in case
self.backup_filepath = Path(
self.hass.config.path(
f"zwavejs_nvm_backup_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.bin"
f"zwavejs_nvm_backup_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.bin" # pylint: disable=home-assistant-enforce-naive-now
)
)
try:
+11 -8
View File
@@ -490,11 +490,11 @@ class _HistoryPrimingManager:
The flush a condition relies on must begin after that condition started
tracking its entities, or the read could miss a change still queued in the
recorder and compute too generous an anchor. A condition therefore never
rides a flush that was already running when it arrived (the lobby); it waits
that one out and joins the next, and re-attempts if the flush it rode was
cancelled before completing. This mirrors `ReloadServiceHelper` minus its
target de-duplication, which does not apply because each condition reads its
own entities.
relies on a flush that was already running when it arrived (the lobby); it
waits that one out and joins the next, re-attempting if the flush it waited
for was cancelled before completing. This mirrors `ReloadServiceHelper`
minus its target de-duplication, which does not apply because each condition
reads its own entities.
"""
def __init__(self, hass: HomeAssistant) -> None:
@@ -516,11 +516,13 @@ class _HistoryPrimingManager:
async def _async_flush(self) -> None:
"""Return once a recorder flush that began no earlier than this call ends.
The first condition of a generation performs the flush; the rest ride it.
The first condition of a generation performs the flush; the rest rely on
it.
"""
async with self._flush_condition:
# Lobby: a flush already running began before we arrived, so it may
# not capture our entity's queued changes. Wait it out, don't ride it.
# not capture our entity's queued changes. Wait it out, don't rely on
# it.
if self._flushing:
await self._flush_condition.wait()
@@ -530,7 +532,8 @@ class _HistoryPrimingManager:
# First past the lobby this generation: we run the flush.
self._flushing = True
break
# A peer began a fresh flush after we cleared the lobby; ride it.
# A peer began a fresh flush after we cleared the lobby; wait for
# it.
await self._flush_condition.wait()
if self._flush_ok:
return
+1 -1
View File
@@ -29,7 +29,7 @@ cached-ipaddress==1.1.2
certifi>=2021.5.30
ciso8601==2.3.3
cronsim==2.7
cryptography==48.0.0
cryptography==48.0.1
dbus-fast==5.0.16
file-read-backwards==2.0.0
fnv-hash-fast==2.0.3
+1 -3
View File
@@ -31,9 +31,7 @@ DISCOVERY_INTEGRATIONS: dict[str, Iterable[str]] = {
}
DEPRECATED_PACKAGES: dict[str, tuple[str, str]] = {
# old_package_name: (reason, breaks_in_ha_version)
"pyserial": ("should be replaced by serialx", "2027.1"),
"pyserial-asyncio": ("should be replaced by serialx", "2027.1"),
"pyserial-asyncio-fast": ("should be replaced by serialx", "2027.1"),
"pyserial-asyncio": ("should be replaced by pyserial-asyncio-fast", "2026.7"),
}
_LOGGER = logging.getLogger(__name__)
+22 -2
View File
@@ -107,6 +107,7 @@ Every check has a code following the
| `W7426` | [`home-assistant-tests-direct-async-unload-entry`](#w7426-home-assistant-tests-direct-async-unload-entry) | Tests should not call an integration's `async_unload_entry` directly |
| `C7414` | [`home-assistant-enforce-utcnow`](#c7414-home-assistant-enforce-utcnow) | Use `homeassistant.util.dt.utcnow` instead of `datetime.now(UTC)` |
| `C7425` | [`home-assistant-enforce-now`](#c7425-home-assistant-enforce-now) | Use `homeassistant.util.dt.now` instead of `datetime.now(<tz>)` |
| `C7427` | [`home-assistant-enforce-naive-now`](#c7427-home-assistant-enforce-naive-now) | Use `homeassistant.util.dt.naive_now` instead of `datetime.now()` |
| `W7423` | [`home-assistant-missing-entity-unique-id`](#w7423-home-assistant-missing-entity-unique-id) | Entity class does not statically guarantee a non-None unique id |
| `W7424` | [`home-assistant-entity-unique-id-static`](#w7424-home-assistant-entity-unique-id-static) | Entity class sets `_attr_unique_id` to a static string at class level |
| `C7412` | [`home-assistant-entity-description-redundant-default`](#c7412-home-assistant-entity-description-redundant-default) | Setting an EntityDescription field to its default value is redundant |
@@ -486,8 +487,27 @@ helper returns an aware `datetime` in the given time zone (defaulting to
`DEFAULT_TIME_ZONE`), keeping the codebase consistent in how the current
local time is obtained. The UTC case (`datetime.now(UTC)`) is handled by
the [`home-assistant-enforce-utcnow`](#c7414-home-assistant-enforce-utcnow)
checker, and `datetime.now()` with no argument is not flagged since it
returns a naive local `datetime`.
checker, and `datetime.now()` with no argument is handled by the
[`home-assistant-enforce-naive-now`](#c7427-home-assistant-enforce-naive-now)
checker.
## `home_assistant_enforce_naive_now` checker
Ensures the Home Assistant helper is used to get the current naive local time.
### `C7427`: `home-assistant-enforce-naive-now`
Use `homeassistant.util.dt.naive_now()` instead of `datetime.datetime.now()`
called without a time zone argument. The helper returns a naive `datetime` in
system local time, keeping the codebase consistent in how the current naive
local time is obtained and documenting that a naive `datetime` is intentional.
An explicit `None` argument (`datetime.now(None)` or `datetime.now(tz=None)`)
returns a naive `datetime` too and is flagged. The aware cases
(`datetime.now(<tz>)` and `datetime.now(UTC)`) are handled by the
[`home-assistant-enforce-now`](#c7425-home-assistant-enforce-now) and
[`home-assistant-enforce-utcnow`](#c7414-home-assistant-enforce-utcnow)
checkers.
## `home_assistant_entity_unique_id` checker
@@ -0,0 +1,45 @@
"""Checker that enforces ``homeassistant.util.dt.naive_now`` over ``datetime.now()``.
Home Assistant exposes ``homeassistant.util.dt.naive_now`` -- a helper that
returns a naive ``datetime`` in system local time. Calling
``datetime.datetime.now()`` with no time zone argument does the same thing but
bypasses the helper. Using ``dt_util.naive_now`` keeps the codebase consistent
in how the current naive local time is obtained and documents that a naive
``datetime`` is intentional.
The aware cases (``datetime.now(tz)`` and ``datetime.now(UTC)``) are handled by
the ``home-assistant-enforce-now`` and ``home-assistant-enforce-utcnow``
checkers respectively.
"""
from pylint.lint import PyLinter
from pylint_home_assistant.helpers.datetime_now import (
CASE_NAIVE,
HassEnforceDatetimeNowChecker,
)
class HassEnforceNaiveNowChecker(HassEnforceDatetimeNowChecker):
"""Checker that flags ``datetime.now()`` calls without a time zone."""
name = "home_assistant_enforce_naive_now"
msgs = {
"C7427": (
"Use `homeassistant.util.dt.naive_now()` instead of `datetime.now()`",
"home-assistant-enforce-naive-now",
"Used when ``datetime.datetime.now()`` is called without a time zone "
"to create a naive ``datetime`` in system local time. Use the "
"``homeassistant.util.dt.naive_now`` helper instead. The aware cases "
"are handled by the ``home-assistant-enforce-now`` and "
"``home-assistant-enforce-utcnow`` checkers.",
),
}
message = "home-assistant-enforce-naive-now"
flagged_case = CASE_NAIVE
def register(linter: PyLinter) -> None:
"""Register the checker."""
linter.register_checker(HassEnforceNaiveNowChecker(linter))
@@ -15,7 +15,10 @@ flagged.
from pylint.lint import PyLinter
from pylint_home_assistant.helpers.datetime_now import HassEnforceDatetimeNowChecker
from pylint_home_assistant.helpers.datetime_now import (
CASE_OTHER,
HassEnforceDatetimeNowChecker,
)
class HassEnforceNowChecker(HassEnforceDatetimeNowChecker):
@@ -34,7 +37,7 @@ class HassEnforceNowChecker(HassEnforceDatetimeNowChecker):
}
message = "home-assistant-enforce-now"
flags_utc = False
flagged_case = CASE_OTHER
def register(linter: PyLinter) -> None:

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