Compare commits

...

10 Commits

Author SHA1 Message Date
Bram Kragten
fd36782bae Bump version to 2025.11.0b1 2025-10-30 20:12:15 +01:00
Bram Kragten
ed4573db57 Update frontend to 20251029.1 (#155513) 2025-10-30 20:11:55 +01:00
Erwin Douna
78373a6483 Firefly fix config flow (#155503) 2025-10-30 20:11:54 +01:00
Sab44
8455c35bec Bump librehardwaremonitor-api to 1.5.0 (#155492) 2025-10-30 20:11:53 +01:00
Kinachi249
00887a2f3f Bump PyCync to 0.4.3 (#155477) 2025-10-30 20:11:52 +01:00
Erwin Douna
f1ca7543fa Bump pyportainer 1.0.12 (#155468) 2025-10-30 20:11:51 +01:00
Abílio Costa
bb72b24ba9 Mock async_setup_entry in BMW Connected Drive config flow test (#155446) 2025-10-30 20:11:50 +01:00
Andrea Turri
322a27d992 Miele RestoreSensor: restore native value rather than stringified state (#152750)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
2025-10-30 20:11:49 +01:00
hanwg
a3b516110b Deprecate legacy Telegram notify service (#150720)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
2025-10-30 20:11:48 +01:00
Bram Kragten
95ac5c0183 Bump version to 2025.11.0b0 2025-10-29 18:53:20 +01:00
19 changed files with 127 additions and 83 deletions

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["pycync==0.4.2"]
"requirements": ["pycync==0.4.3"]
}

View File

@@ -40,7 +40,9 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
client = Firefly(
api_url=data[CONF_URL],
api_key=data[CONF_API_KEY],
session=async_get_clientsession(hass),
session=async_get_clientsession(
hass=hass, verify_ssl=data[CONF_VERIFY_SSL]
),
)
await client.get_about()
except FireflyAuthenticationError:

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251029.0"]
"requirements": ["home-assistant-frontend==20251029.1"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/libre_hardware_monitor",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["librehardwaremonitor-api==1.4.0"]
"requirements": ["librehardwaremonitor-api==1.5.0"]
}

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from typing import Any
from librehardwaremonitor_api.model import LibreHardwareMonitorSensorData
from homeassistant.components.sensor import SensorEntity, SensorStateClass
@@ -51,10 +53,10 @@ class LibreHardwareMonitorSensor(
super().__init__(coordinator)
self._attr_name: str = sensor_data.name
self.value: str | None = sensor_data.value
self._attr_extra_state_attributes: dict[str, str] = {
STATE_MIN_VALUE: self._format_number_value(sensor_data.min),
STATE_MAX_VALUE: self._format_number_value(sensor_data.max),
self._attr_native_value: str | None = sensor_data.value
self._attr_extra_state_attributes: dict[str, Any] = {
STATE_MIN_VALUE: sensor_data.min,
STATE_MAX_VALUE: sensor_data.max,
}
self._attr_native_unit_of_measurement = sensor_data.unit
self._attr_unique_id: str = f"{entry_id}_{sensor_data.sensor_id}"
@@ -72,23 +74,12 @@ class LibreHardwareMonitorSensor(
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if sensor_data := self.coordinator.data.sensor_data.get(self._sensor_id):
self.value = sensor_data.value
self._attr_native_value = sensor_data.value
self._attr_extra_state_attributes = {
STATE_MIN_VALUE: self._format_number_value(sensor_data.min),
STATE_MAX_VALUE: self._format_number_value(sensor_data.max),
STATE_MIN_VALUE: sensor_data.min,
STATE_MAX_VALUE: sensor_data.max,
}
else:
self.value = None
self._attr_native_value = None
super()._handle_coordinator_update()
@property
def native_value(self) -> str | None:
"""Return the formatted sensor value or None if no value is available."""
if self.value is not None and self.value != "-":
return self._format_number_value(self.value)
return None
@staticmethod
def _format_number_value(number_str: str) -> str:
return number_str.replace(",", ".")

View File

@@ -19,7 +19,6 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
STATE_UNKNOWN,
EntityCategory,
UnitOfEnergy,
UnitOfTemperature,
@@ -762,40 +761,35 @@ class MieleSensor(MieleEntity, SensorEntity):
class MieleRestorableSensor(MieleSensor, RestoreSensor):
"""Representation of a Sensor whose internal state can be restored."""
_last_value: StateType
def __init__(
self,
coordinator: MieleDataUpdateCoordinator,
device_id: str,
description: MieleSensorDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, device_id, description)
self._last_value = None
_attr_native_value: StateType
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# recover last value from cache when adding entity
last_value = await self.async_get_last_state()
if last_value and last_value.state != STATE_UNKNOWN:
self._last_value = last_value.state
last_data = await self.async_get_last_sensor_data()
if last_data:
self._attr_native_value = last_data.native_value # type: ignore[assignment]
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self._last_value
"""Return the state of the sensor.
def _update_last_value(self) -> None:
"""Update the last value of the sensor."""
self._last_value = self.entity_description.value_fn(self.device)
It is necessary to override `native_value` to fall back to the default
attribute-based implementation, instead of the function-based
implementation in `MieleSensor`.
"""
return self._attr_native_value
def _update_native_value(self) -> None:
"""Update the native value attribute of the sensor."""
self._attr_native_value = self.entity_description.value_fn(self.device)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_last_value()
self._update_native_value()
super()._handle_coordinator_update()
@@ -912,7 +906,7 @@ class MieleProgramIdSensor(MieleSensor):
class MieleTimeSensor(MieleRestorableSensor):
"""Representation of time sensors keeping state from cache."""
def _update_last_value(self) -> None:
def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
@@ -923,7 +917,9 @@ class MieleTimeSensor(MieleRestorableSensor):
current_status == StateStatus.PROGRAM_ENDED
and self.entity_description.end_value_fn is not None
):
self._last_value = self.entity_description.end_value_fn(self._last_value)
self._attr_native_value = self.entity_description.end_value_fn(
self._attr_native_value
)
# keep value when program ends if no function is specified
elif current_status == StateStatus.PROGRAM_ENDED:
@@ -931,11 +927,11 @@ class MieleTimeSensor(MieleRestorableSensor):
# force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
elif current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
self._last_value = None
self._attr_native_value = None
# otherwise, cache value and return it
else:
self._last_value = current_value
self._attr_native_value = current_value
class MieleConsumptionSensor(MieleRestorableSensor):
@@ -943,13 +939,13 @@ class MieleConsumptionSensor(MieleRestorableSensor):
_is_reporting: bool = False
def _update_last_value(self) -> None:
def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
last_value = (
float(cast(str, self._last_value))
if self._last_value is not None and self._last_value != STATE_UNKNOWN
float(cast(str, self._attr_native_value))
if self._attr_native_value is not None
else 0
)
@@ -963,7 +959,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
StateStatus.SERVICE,
):
self._is_reporting = False
self._last_value = None
self._attr_native_value = None
# appliance might report the last value for consumption of previous cycle and it will report 0
# only after a while, so it is necessary to force 0 until we see the 0 value coming from API, unless
@@ -973,7 +969,7 @@ class MieleConsumptionSensor(MieleRestorableSensor):
and not self._is_reporting
and last_value > 0
):
self._last_value = current_value
self._attr_native_value = current_value
self._is_reporting = True
elif (
@@ -982,12 +978,12 @@ class MieleConsumptionSensor(MieleRestorableSensor):
and current_value is not None
and cast(int, current_value) > 0
):
self._last_value = 0
self._attr_native_value = 0
# keep value when program ends
elif current_status == StateStatus.PROGRAM_ENDED:
pass
else:
self._last_value = current_value
self._attr_native_value = current_value
self._is_reporting = True

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyportainer==1.0.11"]
"requirements": ["pyportainer==1.0.12"]
}

View File

@@ -24,7 +24,8 @@ from homeassistant.components.telegram_bot import (
)
from homeassistant.const import ATTR_LOCATION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.reload import setup_reload_service
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN, PLATFORMS
@@ -45,14 +46,25 @@ PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
)
def get_service(
async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> TelegramNotificationService:
"""Get the Telegram notification service."""
setup_reload_service(hass, DOMAIN, PLATFORMS)
ir.async_create_issue(
hass,
DOMAIN,
"migrate_notify",
breaks_in_ha_version="2026.5.0",
is_fixable=False,
translation_key="migrate_notify",
severity=ir.IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/telegram_bot#notifiers",
)
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
chat_id = config.get(CONF_CHAT_ID)
return TelegramNotificationService(hass, chat_id)

View File

@@ -1,4 +1,10 @@
{
"issues": {
"migrate_notify": {
"description": "The Telegram `notify` service has been migrated. A new `notify` entity per chat ID is available now.\n\nUpdate all affected automations to use the new `notify.send_message` action exposed by these new entities and then restart Home Assistant.",
"title": "Migration of Telegram notify service"
}
},
"services": {
"reload": {
"description": "Reloads telegram notify services.",

View File

@@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 11
PATCH_VERSION: Final = "0.dev0"
PATCH_VERSION: Final = "0b1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

@@ -39,7 +39,7 @@ habluetooth==5.7.0
hass-nabucasa==1.5.1
hassil==3.4.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20251029.0
home-assistant-frontend==20251029.1
home-assistant-intents==2025.10.28
httpx==0.28.1
ifaddr==0.2.0

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.11.0.dev0"
version = "2025.11.0b1"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."

8
requirements_all.txt generated
View File

@@ -1201,7 +1201,7 @@ hole==0.9.0
holidays==0.83
# homeassistant.components.frontend
home-assistant-frontend==20251029.0
home-assistant-frontend==20251029.1
# homeassistant.components.conversation
home-assistant-intents==2025.10.28
@@ -1376,7 +1376,7 @@ libpyfoscamcgi==0.0.8
libpyvivotek==0.6.1
# homeassistant.components.libre_hardware_monitor
librehardwaremonitor-api==1.4.0
librehardwaremonitor-api==1.5.0
# homeassistant.components.mikrotik
librouteros==3.2.0
@@ -1948,7 +1948,7 @@ pycsspeechtts==1.0.8
# pycups==2.0.4
# homeassistant.components.cync
pycync==0.4.2
pycync==0.4.3
# homeassistant.components.daikin
pydaikin==2.17.1
@@ -2305,7 +2305,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.11
pyportainer==1.0.12
# homeassistant.components.probe_plus
pyprobeplus==1.1.2

View File

@@ -1050,7 +1050,7 @@ hole==0.9.0
holidays==0.83
# homeassistant.components.frontend
home-assistant-frontend==20251029.0
home-assistant-frontend==20251029.1
# homeassistant.components.conversation
home-assistant-intents==2025.10.28
@@ -1195,7 +1195,7 @@ letpot==0.6.3
libpyfoscamcgi==0.0.8
# homeassistant.components.libre_hardware_monitor
librehardwaremonitor-api==1.4.0
librehardwaremonitor-api==1.5.0
# homeassistant.components.mikrotik
librouteros==3.2.0
@@ -1641,7 +1641,7 @@ pycsspeechtts==1.0.8
# pycups==2.0.4
# homeassistant.components.cync
pycync==0.4.2
pycync==0.4.3
# homeassistant.components.daikin
pydaikin==2.17.1
@@ -1932,7 +1932,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.11
pyportainer==1.0.12
# homeassistant.components.probe_plus
pyprobeplus==1.1.2

View File

@@ -274,10 +274,14 @@ async def test_reauth(hass: HomeAssistant) -> None:
async def test_reconfigure(hass: HomeAssistant) -> None:
"""Test the reconfiguration form."""
with patch(
BIMMER_CONNECTED_LOGIN_PATCH,
side_effect=login_sideeffect,
autospec=True,
with (
patch(
BIMMER_CONNECTED_LOGIN_PATCH, side_effect=login_sideeffect, autospec=True
),
patch(
"homeassistant.components.bmw_connected_drive.async_setup_entry",
return_value=True,
),
):
config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY)
config_entry.add_to_hass(hass)

View File

@@ -634,8 +634,8 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Fan',
'max_value': '-',
'min_value': '-',
'max_value': None,
'min_value': None,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
@@ -1458,8 +1458,8 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Fan',
'max_value': '-',
'min_value': '-',
'max_value': None,
'min_value': None,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
@@ -1836,8 +1836,8 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Fan',
'max_value': '-',
'min_value': '-',
'max_value': None,
'min_value': None,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,

View File

@@ -100,7 +100,7 @@ async def test_sensors_are_updated(
updated_data = dict(mock_lhm_client.get_data.return_value.sensor_data)
updated_data["amdcpu-0-temperature-3"] = replace(
updated_data["amdcpu-0-temperature-3"], value="42,1"
updated_data["amdcpu-0-temperature-3"], value="42.1"
)
mock_lhm_client.get_data.return_value = replace(
mock_lhm_client.get_data.return_value,

View File

@@ -10,13 +10,15 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.miele.const import DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
async_load_json_object_fixture,
mock_restore_cache_with_extra_data,
snapshot_platform,
)
@@ -583,6 +585,7 @@ async def test_laundry_dry_scenario(
check_sensor_state(hass, "sensor.tumble_dryer_elapsed_time", "20", step)
@pytest.mark.parametrize("restore_state", ["45", STATE_UNKNOWN, STATE_UNAVAILABLE])
@pytest.mark.parametrize("load_device_file", ["laundry.json"])
@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)])
async def test_elapsed_time_sensor_restored(
@@ -592,6 +595,7 @@ async def test_elapsed_time_sensor_restored(
setup_platform: None,
device_fixture: MieleDevices,
freezer: FrozenDateTimeFactory,
restore_state,
) -> None:
"""Test that elapsed time returns the restored value when program ended."""
@@ -648,6 +652,26 @@ async def test_elapsed_time_sensor_restored(
assert hass.states.get(entity_id).state == "unavailable"
# simulate restore with state different from native value
mock_restore_cache_with_extra_data(
hass,
[
(
State(
entity_id,
restore_state,
{
"unit_of_measurement": "min",
},
),
{
"native_value": "12",
"native_unit_of_measurement": "min",
},
),
],
)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()

View File

@@ -7,12 +7,15 @@ from homeassistant.components import notify
from homeassistant.components.telegram import DOMAIN
from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from tests.common import get_fixture_path
async def test_reload_notify(hass: HomeAssistant) -> None:
async def test_reload_notify(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Verify we can reload the notify service."""
with patch("homeassistant.components.telegram_bot.async_setup", return_value=True):
@@ -45,3 +48,9 @@ async def test_reload_notify(hass: HomeAssistant) -> None:
assert not hass.services.has_service(notify.DOMAIN, DOMAIN)
assert hass.services.has_service(notify.DOMAIN, "telegram_reloaded")
assert issue_registry.async_get_issue(
domain=DOMAIN,
issue_id="migrate_notify",
)
assert len(issue_registry.issues) == 1