Compare commits

...

31 Commits

Author SHA1 Message Date
Franck Nijhof
b981ece163 Pin actions/helpers/info to fix release build (#167327) 2026-04-03 20:58:54 +00:00
Franck Nijhof
7ea931fdc8 2026.4.1 (#167310) 2026-04-03 22:10:54 +02:00
Franck Nijhof
f3038a20af Bump version to 2026.4.1 2026-04-03 16:05:08 +00:00
Pete Sage
de234c7190 Sonos alarm switch entities may not be created when speaker offline initially (#167303) 2026-04-03 16:01:17 +00:00
Pete Sage
399681984f Bump soco to 0.30.15 (#167299) 2026-04-03 16:01:16 +00:00
Joost Lekkerkerker
5ca14ca7d7 Bump Zinvolt to 0.4.1 (#167296) 2026-04-03 16:01:15 +00:00
Joost Lekkerkerker
ac53cfa85a Make sure we take all Zinvolt battery units in account (#167294) 2026-04-03 16:01:13 +00:00
Ludovic BOUÉ
02f1a9c3a9 Fix Matter water heater off mode (#167286)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 16:01:12 +00:00
Bram Kragten
f93fdceac9 Update frontend to 20260325.6 (#167285) 2026-04-03 16:01:11 +00:00
Ludovic BOUÉ
711a89f7b8 Fix to allow Matter Fan percent setting to be null when FanMode is Auto (#167279)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 16:01:09 +00:00
Norbert Rittel
19e58c554e Improve Assist satellite action naming consistency (#167278) 2026-04-03 16:01:08 +00:00
Joost Lekkerkerker
feb6c2bfe6 Bump zinvolt to 0.4.0 (#167276) 2026-04-03 16:01:07 +00:00
Norbert Rittel
6bb91422ff Improve Media player action naming consistency (#167274)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-03 16:01:06 +00:00
Andrew Jackson
3bd699285b Remove Transmission port forward sensor (#167269) 2026-04-03 16:00:17 +00:00
dotlambda
6d10305197 Bump psutil to 7.2.2 (#167263) 2026-04-03 15:57:57 +00:00
Joakim Plate
42a9c8488d Update arcam to 1.8.3 (#167249) 2026-04-03 15:57:56 +00:00
Norbert Rittel
c6c273559e Improve Recorder action naming consistency (#167244) 2026-04-03 15:57:55 +00:00
Pete Sage
f7394ce302 Fix Sonos reporting wrong state when media title is whitespace (#167223) 2026-04-03 15:57:53 +00:00
G Johansson
175dec6f1a Bump holiday library to 0.93 (#167217) 2026-04-03 15:57:52 +00:00
G Johansson
d137761cb5 Fix SMHI (#167212) 2026-04-03 15:57:50 +00:00
Simone Chemelli
8055cbc58d Migrate image unique_id for Fritz (#167209) 2026-04-03 15:57:49 +00:00
Joost Lekkerkerker
c9dff27590 Remove not implemented supported feature from Wiim (#167205) 2026-04-03 15:57:48 +00:00
Mike Degatano
c913a858b6 Wrap hassio import in is_hassio check in get_system_info helper (#167111) 2026-04-03 15:57:46 +00:00
Joost Lekkerkerker
4ed33a804e Bump pySmartThings to 3.7.3 (#167075) 2026-04-03 15:57:45 +00:00
Kevin O'Brien
8bf5674826 Fix Proxmox VE backup status sensor false positive due to case mismatch (#167069)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 15:57:43 +00:00
Manu
b8a0b0083b Fix websocket calling async_release_notes in update component although unavailable (#167067) 2026-04-03 15:57:42 +00:00
Bram Kragten
a57c101b5e Fix select condition state selector (#167064) 2026-04-03 15:57:41 +00:00
Brett Adams
957b8c1c52 Fix Tesla Fleet OAuth scope refresh during reauth (#166920) 2026-04-03 15:57:40 +00:00
Brett Adams
bb002d051b Fix Tesla Fleet charge current scope handling (#166919) 2026-04-03 15:57:38 +00:00
LTek
2b2fd4ac92 Fix Ring snapshots (#164337)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-03 15:57:37 +00:00
Jan Bouwhuis
f4c270629b Fix tuya energy sensor units (#160392) 2026-04-03 15:57:35 +00:00
78 changed files with 6885 additions and 11793 deletions

View File

@@ -49,7 +49,7 @@ jobs:
- name: Get information
id: info
uses: home-assistant/actions/helpers/info@master # zizmor: ignore[unpinned-uses]
uses: home-assistant/actions/helpers/info@5f5b077d63a1e4c53019231409a0c4d791fb74e5 # zizmor: ignore[unpinned-uses]
- name: Get version
id: version

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["arcam"],
"requirements": ["arcam-fmj==1.8.2"],
"requirements": ["arcam-fmj==1.8.3"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@@ -91,6 +91,7 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
value_fn=lambda state: (
vp.colorspace.name.lower()
if (vp := state.get_incoming_video_parameters()) is not None
and vp.colorspace is not None
else None
),
),

View File

@@ -75,7 +75,7 @@
},
"services": {
"announce": {
"description": "Lets a satellite announce a message.",
"description": "Lets an Assist satellite announce a message.",
"fields": {
"media_id": {
"description": "The media ID to announce instead of using text-to-speech.",
@@ -94,10 +94,10 @@
"name": "Preannounce media ID"
}
},
"name": "Announce"
"name": "Announce on satellite"
},
"ask_question": {
"description": "Asks a question and gets the user's response.",
"description": "Lets an Assist satellite ask a question and get the user's response.",
"fields": {
"answers": {
"description": "Possible answers to the question.",
@@ -124,10 +124,10 @@
"name": "Question media ID"
}
},
"name": "Ask question"
"name": "Ask question on satellite"
},
"start_conversation": {
"description": "Starts a conversation from a satellite.",
"description": "Starts a conversation from an Assist satellite.",
"fields": {
"extra_system_prompt": {
"description": "Provide background information to the AI about the request.",
@@ -150,13 +150,13 @@
"name": "Message"
}
},
"name": "Start conversation"
"name": "Start conversation on satellite"
}
},
"title": "Assist satellite",
"triggers": {
"idle": {
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
"description": "Triggers after one or more Assist satellites become idle after having processed a command.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -165,7 +165,7 @@
"name": "Satellite became idle"
},
"listening": {
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
"description": "Triggers after one or more Assist satellites start listening for a command from someone.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -174,7 +174,7 @@
"name": "Satellite started listening"
},
"processing": {
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
"description": "Triggers after one or more Assist satellites start processing a command after having heard it.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -183,7 +183,7 @@
"name": "Satellite started processing"
},
"responding": {
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
"description": "Triggers after one or more Assist satellites start responding to a command after having processed it, or start announcing something.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"

View File

@@ -10,9 +10,11 @@ from requests.exceptions import RequestException
from homeassistant.components.image import ImageEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util, slugify
from .const import DOMAIN, Platform
from .coordinator import AvmWrapper, FritzConfigEntry
from .entity import FritzBoxBaseEntity
@@ -22,6 +24,32 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
async def _migrate_to_new_unique_id(
hass: HomeAssistant, avm_wrapper: AvmWrapper, ssid: str
) -> None:
"""Migrate old unique id to new unique id."""
old_unique_id = slugify(f"{avm_wrapper.unique_id}-{ssid}-qr-code")
new_unique_id = f"{avm_wrapper.unique_id}-guest_wifi_qr_code"
entity_registry = er.async_get(hass)
entity_id = entity_registry.async_get_entity_id(
Platform.IMAGE,
DOMAIN,
old_unique_id,
)
if entity_id is None:
return
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
_LOGGER.debug(
"Migrating guest Wi-Fi image unique_id from [%s] to [%s]",
old_unique_id,
new_unique_id,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FritzConfigEntry,
@@ -34,6 +62,8 @@ async def async_setup_entry(
avm_wrapper.fritz_guest_wifi.get_info
)
await _migrate_to_new_unique_id(hass, avm_wrapper, guest_wifi_info["NewSSID"])
async_add_entities(
[
FritzGuestWifiQRImage(
@@ -60,7 +90,7 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
) -> None:
"""Initialize the image entity."""
self._attr_name = ssid
self._attr_unique_id = slugify(f"{avm_wrapper.unique_id}-{ssid}-qr-code")
self._attr_unique_id = f"{avm_wrapper.unique_id}-guest_wifi_qr_code"
self._current_qr_bytes: bytes | None = None
super().__init__(avm_wrapper, device_friendly_name)
ImageEntity.__init__(self, hass)

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260325.5"]
"requirements": ["home-assistant-frontend==20260325.6"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.84", "babel==2.15.0"]
"requirements": ["holidays==0.93", "babel==2.15.0"]
}

View File

@@ -323,7 +323,11 @@ DISCOVERY_SCHEMAS = [
required_attributes=(
clusters.FanControl.Attributes.FanMode,
clusters.FanControl.Attributes.PercentCurrent,
clusters.FanControl.Attributes.PercentSetting,
),
# PercentSetting SHALL be null when FanMode is Auto (spec 4.4.6.3),
# so allow null values to not block discovery in that state.
allow_none_value=True,
optional_attributes=(
clusters.FanControl.Attributes.SpeedSetting,
clusters.FanControl.Attributes.RockSetting,

View File

@@ -168,10 +168,15 @@ class MatterWaterHeater(MatterEntity, WaterHeaterEntity):
self._attr_target_temperature = self._get_temperature_in_degrees(
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
)
system_mode = self.get_matter_attribute_value(
clusters.Thermostat.Attributes.SystemMode
)
boost_state = self.get_matter_attribute_value(
clusters.WaterHeaterManagement.Attributes.BoostState
)
if boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
if system_mode == clusters.Thermostat.Enums.SystemModeEnum.kOff:
self._attr_current_operation = STATE_OFF
elif boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
self._attr_current_operation = STATE_HIGH_DEMAND
else:
self._attr_current_operation = STATE_ECO
@@ -218,6 +223,7 @@ DISCOVERY_SCHEMAS = [
clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit,
clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit,
clusters.Thermostat.Attributes.LocalTemperature,
clusters.Thermostat.Attributes.SystemMode,
clusters.WaterHeaterManagement.Attributes.FeatureMap,
),
optional_attributes=(

View File

@@ -260,7 +260,7 @@
},
"clear_playlist": {
"description": "Removes all items from a media player's playlist.",
"name": "Clear playlist"
"name": "Clear media player playlist"
},
"join": {
"description": "Groups media players together for synchronous playback. Only works on supported multiroom audio systems.",
@@ -270,44 +270,44 @@
"name": "Group members"
}
},
"name": "Join"
"name": "Join media players"
},
"media_next_track": {
"description": "Selects the next track.",
"name": "Next"
"description": "Selects the next track on a media player.",
"name": "Next track"
},
"media_pause": {
"description": "Pauses playback on a media player.",
"name": "[%key:common::action::pause%]"
"name": "Pause media"
},
"media_play": {
"description": "Starts playback on a media player.",
"name": "Play"
"name": "Play media"
},
"media_play_pause": {
"description": "Toggles play/pause on a media player.",
"name": "Play/Pause"
"name": "Play/Pause media"
},
"media_previous_track": {
"description": "Selects the previous track.",
"name": "Previous"
"description": "Selects the previous track on a media player.",
"name": "Previous track"
},
"media_seek": {
"description": "Allows you to go to a different part of the media that is currently playing.",
"description": "Allows you to go to a different part of the media that is currently playing on a media player.",
"fields": {
"seek_position": {
"description": "Target position in the currently playing media. The format is platform dependent.",
"name": "Position"
}
},
"name": "Seek"
"name": "Seek media"
},
"media_stop": {
"description": "Stops playback on a media player.",
"name": "[%key:common::action::stop%]"
"name": "Stop media"
},
"play_media": {
"description": "Starts playing specified media.",
"description": "Starts playing specified media on a media player.",
"fields": {
"announce": {
"description": "If the media should be played as an announcement.",
@@ -325,14 +325,14 @@
"name": "Play media"
},
"repeat_set": {
"description": "Sets the repeat mode.",
"description": "Sets the repeat mode of a media player.",
"fields": {
"repeat": {
"description": "Whether the media (one or all) should be played in a loop or not.",
"name": "Repeat mode"
}
},
"name": "Set repeat"
"name": "Set media player repeat"
},
"search_media": {
"description": "Searches the available media.",
@@ -357,14 +357,14 @@
"name": "Search media"
},
"select_sound_mode": {
"description": "Selects a specific sound mode.",
"description": "Selects a specific sound mode of a media player.",
"fields": {
"sound_mode": {
"description": "Name of the sound mode to switch to.",
"name": "Sound mode"
}
},
"name": "Select sound mode"
"name": "Select media player sound mode"
},
"select_source": {
"description": "Sends a media player the command to change the input source.",
@@ -374,37 +374,37 @@
"name": "Source"
}
},
"name": "Select source"
"name": "Select media player source"
},
"shuffle_set": {
"description": "Enables or disables the shuffle mode.",
"description": "Enables or disables the shuffle mode of a media player.",
"fields": {
"shuffle": {
"description": "Whether the media should be played in randomized order or not.",
"name": "Shuffle mode"
}
},
"name": "Set shuffle"
"name": "Set media player shuffle"
},
"toggle": {
"description": "Toggles a media player on/off.",
"name": "[%key:common::action::toggle%]"
"name": "Toggle media player"
},
"turn_off": {
"description": "Turns off the power of a media player.",
"name": "[%key:common::action::turn_off%]"
"name": "Turn off media player"
},
"turn_on": {
"description": "Turns on the power of a media player.",
"name": "[%key:common::action::turn_on%]"
"name": "Turn on media player"
},
"unjoin": {
"description": "Removes a media player from a group. Only works on platforms which support player groups.",
"name": "Unjoin"
"name": "Unjoin media player"
},
"volume_down": {
"description": "Turns down the volume of a media player.",
"name": "Turn down volume"
"name": "Turn down media player volume"
},
"volume_mute": {
"description": "Mutes or unmutes a media player.",
@@ -414,7 +414,7 @@
"name": "Muted"
}
},
"name": "Mute/unmute volume"
"name": "Mute/unmute media player"
},
"volume_set": {
"description": "Sets the volume level of a media player.",
@@ -424,11 +424,11 @@
"name": "Level"
}
},
"name": "Set volume"
"name": "Set media player volume"
},
"volume_up": {
"description": "Turns up the volume of a media player.",
"name": "Turn up volume"
"name": "Turn up media player volume"
}
},
"title": "Media player",

View File

@@ -21,7 +21,7 @@ VM_CONTAINER_RUNNING = "running"
STORAGE_ACTIVE = 1
STORAGE_SHARED = 1
STORAGE_ENABLED = 1
STATUS_OK = "ok"
STATUS_OK = "OK"
AUTH_PAM = "pam"
AUTH_PVE = "pve"

View File

@@ -12,11 +12,11 @@
"services": {
"disable": {
"description": "Stops the recording of events and state changes.",
"name": "[%key:common::action::disable%]"
"name": "Disable Recorder"
},
"enable": {
"description": "Starts the recording of events and state changes.",
"name": "[%key:common::action::enable%]"
"name": "Enable Recorder"
},
"get_statistics": {
"description": "Retrieves statistics data for entities within a specific time period.",
@@ -46,7 +46,7 @@
"name": "Units"
}
},
"name": "Get statistics"
"name": "Get Recorder statistics"
},
"purge": {
"description": "Starts purge task - to clean up old data from your database.",
@@ -64,7 +64,7 @@
"name": "Repack"
}
},
"name": "Purge"
"name": "Purge Recorder database"
},
"purge_entities": {
"description": "Starts a purge task to remove the data related to specific entities from your database.",
@@ -86,7 +86,7 @@
"name": "[%key:component::recorder::services::purge::fields::keep_days::name%]"
}
},
"name": "Purge entities"
"name": "Purge Recorder entities"
}
},
"system_health": {

View File

@@ -128,9 +128,8 @@ class RingCam(RingEntity[RingDoorBell], Camera):
self._device = self._get_coordinator_data().get_video_device(
self._device.device_api_id
)
history_data = self._device.last_history
if history_data and self._device.has_subscription:
if history_data:
self._last_event = history_data[0]
# will call async_update to update the attributes and get the
# video url from the api
@@ -155,16 +154,13 @@ class RingCam(RingEntity[RingDoorBell], Camera):
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
if self._video_url is None:
if not self._device.has_subscription:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="no_subscription",
)
return None
# For live_view cameras, get a fresh snapshot
if self.entity_description.key == "live_view":
return await self._async_get_fresh_snapshot()
# For last_recording cameras, use the cached video frame
key = (width, height)
if not (image := self._images.get(key)):
if not (image := self._images.get(key)) and self._video_url is not None:
image = await ffmpeg.async_get_image(
self.hass,
self._video_url,
@@ -177,6 +173,11 @@ class RingCam(RingEntity[RingDoorBell], Camera):
return image
@exception_wrap
async def _async_get_fresh_snapshot(self) -> bytes | None:
"""Get a fresh snapshot from the camera."""
return await self._device.async_get_snapshot()
async def handle_async_mjpeg_stream(
self, request: web.Request
) -> web.StreamResponse | None:

View File

@@ -151,9 +151,6 @@
"api_timeout": {
"message": "Timeout communicating with Ring API"
},
"no_subscription": {
"message": "Ring Protect subscription required for snapshots"
},
"sdp_m_line_index_required": {
"message": "Error negotiating stream for {device}"
}

View File

@@ -19,7 +19,6 @@ is_option_selected:
required: true
selector:
state:
attribute: options
hide_states:
- unavailable
- unknown

View File

@@ -38,5 +38,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.7.2"]
"requirements": ["pysmartthings==3.7.3"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pysmhi"],
"requirements": ["pysmhi==1.1.0"]
"requirements": ["pysmhi==2.0.0"]
}

View File

@@ -56,6 +56,21 @@ FORESTDRY_MAP = {
"5": "very_dry",
"6": "extremely_dry",
}
PRECIPITATION_CATEGORY_MAP = {
0: "no_precipitation",
1: "rain",
2: "thunderstorm",
3: "freezing_rain",
4: "mixed_ice",
5: "snow",
6: "wet_snow",
7: "rain_snow_mixed",
8: "ice_pellets",
9: "graupel",
10: "hail",
11: "drizzle",
12: "freezing_drizzle",
}
def get_percentage_values(entity: SMHIWeatherSensor, key: str) -> int | None:
@@ -68,6 +83,14 @@ def get_percentage_values(entity: SMHIWeatherSensor, key: str) -> int | None:
return None
def get_precipitation_category(entity: SMHIWeatherSensor) -> str | None:
"""Return the precipitation category."""
value: int | None = entity.coordinator.current.get("precipitation_category")
if value in PRECIPITATION_CATEGORY_MAP:
return PRECIPITATION_CATEGORY_MAP[value]
return None
def get_fire_index_value(entity: SMHIFireSensor, key: str) -> str:
"""Return index value as string."""
value: int | None = entity.coordinator.fire_current.get(key) # type: ignore[assignment]
@@ -128,11 +151,9 @@ WEATHER_SENSOR_DESCRIPTIONS: tuple[SMHIWeatherEntityDescription, ...] = (
SMHIWeatherEntityDescription(
key="precipitation_category",
translation_key="precipitation_category",
value_fn=lambda entity: str(
get_percentage_values(entity, "precipitation_category")
),
value_fn=get_precipitation_category,
device_class=SensorDeviceClass.ENUM,
options=["0", "1", "2", "3", "4", "5", "6"],
options=[*PRECIPITATION_CATEGORY_MAP.values()],
),
SMHIWeatherEntityDescription(
key="frozen_precipitation",

View File

@@ -95,13 +95,19 @@
"precipitation_category": {
"name": "Precipitation category",
"state": {
"0": "No precipitation",
"1": "Snow",
"2": "Snow and rain",
"3": "Rain",
"4": "Drizzle",
"5": "Freezing rain",
"6": "Freezing drizzle"
"drizzle": "Drizzle",
"freezing_drizzle": "Freezing drizzle",
"freezing_rain": "Freezing rain",
"graupel": "Graupel",
"hail": "Hail",
"ice_pellets": "Ice pellets",
"mixed_ice": "Mixed/ice",
"no_precipitation": "No precipitation",
"rain": "Rain",
"rain_snow_mixed": "Mixture of rain and snow",
"snow": "Snow",
"thunderstorm": "Thunderstorm",
"wet_snow": "Wet snow"
}
},
"rate_of_spread": {

View File

@@ -417,6 +417,7 @@ class SonosDiscoveryManager:
)
new_coordinator.setup(soco)
c_dict[soco.household_id] = new_coordinator
c_dict[soco.household_id].add_speaker(soco)
speaker.setup(self.entry)
except (OSError, SoCoException, Timeout) as ex:
_LOGGER.warning("Failed to add SonosSpeaker using %s: %s", soco, ex)

View File

@@ -99,3 +99,7 @@ class SonosAlarms(SonosHouseholdCoordinator):
)
self.last_processed_event_id = self.alarms.last_id
return True
def add_speaker(self, soco: SoCo) -> None:
"""Update any skipped alarms when speaker is added."""
self.alarms.update_skipped(soco)

View File

@@ -85,3 +85,6 @@ class SonosHouseholdCoordinator:
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
"""Update the cache of the household-level feature and return if cache has changed."""
raise NotImplementedError
def add_speaker(self, soco: SoCo) -> None:
"""Additional processing when a speaker is added if needed."""

View File

@@ -12,7 +12,7 @@
"quality_scale": "bronze",
"requirements": [
"defusedxml==0.7.1",
"soco==0.30.14",
"soco==0.30.15",
"sonos-websocket==0.1.3"
],
"ssdp": [

View File

@@ -132,7 +132,8 @@ class SonosMedia:
self.artist = track_info.get("artist")
self.album_name = track_info.get("album")
self.title = track_info.get("title")
title = track_info.get("title") or ""
self.title = title.strip() or None
self.image_url = track_info.get("album_art")
playlist_position = int(track_info.get("playlist_position", -1))

View File

@@ -9,7 +9,7 @@ import os
from typing import TYPE_CHECKING, Any, NamedTuple
from psutil import Process
from psutil._common import sbattery, sdiskusage, shwtemp, snetio, snicaddr, sswap
from psutil._ntuples import sbattery, sdiskusage, shwtemp, snetio, snicaddr, sswap
import psutil_home_assistant as ha_psutil
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/systemmonitor",
"iot_class": "local_push",
"loggers": ["psutil"],
"requirements": ["psutil-home-assistant==0.0.1", "psutil==7.1.2"],
"requirements": ["psutil-home-assistant==0.0.1", "psutil==7.2.2"],
"single_config_entry": true
}

View File

@@ -5,7 +5,7 @@ import os
import re
from typing import Any
from psutil._common import sfan, shwtemp
from psutil._ntuples import sfan, shwtemp
import psutil_home_assistant as ha_psutil
from homeassistant.core import HomeAssistant

View File

@@ -52,7 +52,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslaFleetNumberVehicleEntityDescription, ...] = (
mode=NumberMode.AUTO,
max_key="charge_state_charge_current_request_max",
func=lambda api, value: api.set_charging_amps(value),
scopes=[Scope.VEHICLE_CHARGING_CMDS],
scopes=[Scope.VEHICLE_CHARGING_CMDS, Scope.VEHICLE_CMDS],
),
TeslaFleetNumberVehicleEntityDescription(
key="charge_state_charge_limit_soc",

View File

@@ -30,4 +30,8 @@ class TeslaUserImplementation(AuthImplementation):
@property
def extra_authorize_data(self) -> dict[str, Any]:
"""Extra data that needs to be appended to the authorize url."""
return {"prompt": "login", "scope": " ".join(SCOPES)}
return {
"prompt": "login",
"prompt_missing_scopes": "true",
"scope": " ".join(SCOPES),
}

View File

@@ -50,7 +50,7 @@
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"reauth_confirm": {
"description": "The {name} integration needs to re-authenticate your account",
"description": "The {name} integration needs to re-authenticate your account. Reauthentication refreshes the Tesla API permissions granted to Home Assistant, including any newly enabled scopes.",
"title": "[%key:common::config_flow::title::reauth%]"
},
"registration_complete": {
@@ -60,7 +60,7 @@
"data_description": {
"qr_code": "Scan this QR code with your phone to set up the virtual key."
},
"description": "To enable command signing, you must open the Tesla app, select your vehicle, and then visit the following URL to set up a virtual key. You must repeat this process for each vehicle.\n\n{virtual_key_url}",
"description": "To enable command signing, you must open the Tesla app, select your vehicle, and then visit the following URL to set up a virtual key. You must repeat this process for each vehicle.\n\n{virtual_key_url}\n\nIf you later enable additional Tesla API permissions, reauthenticate the integration to refresh the granted scopes.",
"title": "Command signing"
}
}

View File

@@ -40,7 +40,7 @@ from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
MIGRATION_NAME_TO_KEY = {
# Sensors

View File

@@ -1,64 +0,0 @@
"""Binary sensor platform for Transmission integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import TransmissionConfigEntry, TransmissionDataUpdateCoordinator
from .entity import TransmissionEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class TransmissionBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describe a Transmission binary sensor entity."""
is_on_fn: Callable[[TransmissionDataUpdateCoordinator], bool | None]
BINARY_SENSOR_TYPES: tuple[TransmissionBinarySensorEntityDescription, ...] = (
TransmissionBinarySensorEntityDescription(
key="port_forwarding",
translation_key="port_forwarding",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
is_on_fn=lambda coordinator: coordinator.port_forwarding,
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: TransmissionConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Transmission binary sensors from a config entry."""
coordinator = config_entry.runtime_data
async_add_entities(
TransmissionBinarySensor(coordinator, description)
for description in BINARY_SENSOR_TYPES
)
class TransmissionBinarySensor(TransmissionEntity, BinarySensorEntity):
"""Representation of a Transmission binary sensor."""
entity_description: TransmissionBinarySensorEntityDescription
@property
def is_on(self) -> bool | None:
"""Return True if the port is open."""
return self.entity_description.is_on_fn(self.coordinator)

View File

@@ -45,7 +45,6 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
self.api = api
self.host = entry.data[CONF_HOST]
self._session: transmission_rpc.Session | None = None
self.port_forwarding: bool | None = None
self._all_torrents: list[transmission_rpc.Torrent] = []
self._completed_torrents: list[transmission_rpc.Torrent] = []
self._started_torrents: list[transmission_rpc.Torrent] = []
@@ -78,7 +77,6 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
data = self.api.session_stats()
self.torrents = self.api.get_torrents()
self._session = self.api.get_session()
self.port_forwarding = self.api.port_test()
except transmission_rpc.TransmissionError as err:
raise UpdateFailed("Unable to connect to Transmission client") from err

View File

@@ -1,13 +1,5 @@
{
"entity": {
"binary_sensor": {
"port_forwarding": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
}
}
},
"sensor": {
"active_torrents": {
"default": "mdi:counter"

View File

@@ -41,15 +41,6 @@
}
},
"entity": {
"binary_sensor": {
"port_forwarding": {
"name": "Port forwarding",
"state": {
"off": "Closed",
"on": "Open"
}
}
},
"sensor": {
"active_torrents": {
"name": "Active torrents",

View File

@@ -35,6 +35,7 @@ from homeassistant.const import (
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfTime,
)
@@ -656,6 +657,8 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="total_energy",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
TuyaSensorEntityDescription(
key=DPCode.PRO_ADD_ELE,

View File

@@ -531,7 +531,13 @@ async def websocket_release_notes(
"Entity does not support release notes",
)
return
if entity.available is False:
connection.send_error(
msg["id"],
websocket_api.ERR_HOME_ASSISTANT_ERROR,
"Entity is not available",
)
return
connection.send_result(
msg["id"],
await entity.async_release_notes(),

View File

@@ -66,7 +66,6 @@ SUPPORT_WIIM_BASE = (
| MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.GROUPING
| MediaPlayerEntityFeature.SEEK
)

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.84"]
"requirements": ["holidays==0.93"]
}

View File

@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ZinvoltConfigEntry, ZinvoltData, ZinvoltDeviceCoordinator
from .entity import ZinvoltEntity
from .entity import ZinvoltEntity, ZinvoltUnitEntity
POINT_ENTITIES = {
"communication": BinarySensorDeviceClass.PROBLEM,
@@ -57,9 +57,10 @@ async def async_setup_entry(
for coordinator in entry.runtime_data.values()
]
entities.extend(
ZinvoltPointBinarySensor(coordinator, point)
ZinvoltPointBinarySensor(coordinator, battery.serial_number, point)
for coordinator in entry.runtime_data.values()
for point in coordinator.data.points
for battery in coordinator.battery_units.values()
for point in coordinator.data.batteries[battery.serial_number].points
if point in POINT_ENTITIES
)
async_add_entities(entities)
@@ -88,25 +89,27 @@ class ZinvoltBatteryStateBinarySensor(ZinvoltEntity, BinarySensorEntity):
return self.entity_description.is_on_fn(self.coordinator.data)
class ZinvoltPointBinarySensor(ZinvoltEntity, BinarySensorEntity):
class ZinvoltPointBinarySensor(ZinvoltUnitEntity, BinarySensorEntity):
"""Zinvolt battery state binary sensor."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, coordinator: ZinvoltDeviceCoordinator, point: str) -> None:
def __init__(
self, coordinator: ZinvoltDeviceCoordinator, unit_serial_number: str, point: str
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
super().__init__(coordinator, unit_serial_number)
self.point = point
self._attr_translation_key = point
self._attr_device_class = POINT_ENTITIES[point]
self._attr_unique_id = f"{coordinator.data.battery.serial_number}.{point}"
self._attr_unique_id = f"{self.serial_number}.{point}"
@property
def available(self) -> bool:
"""Return the availability of the binary sensor."""
return super().available and self.point in self.coordinator.data.points
return super().available and self.point in self.battery.points
@property
def is_on(self) -> bool:
"""Return the state of the binary sensor."""
return not self.coordinator.data.points[self.point]
return not self.battery.points[self.point]

View File

@@ -6,7 +6,7 @@ import logging
from zinvolt import ZinvoltClient
from zinvolt.exceptions import ZinvoltError
from zinvolt.models import Battery, BatteryState
from zinvolt.models import Battery, BatteryState, Unit, UnitType
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -24,6 +24,13 @@ class ZinvoltData:
"""Data for the Zinvolt integration."""
battery: BatteryState
batteries: dict[str, BatteryData]
@dataclass
class BatteryData:
"""Data per battery unit."""
sw_version: str
model: str
points: dict[str, bool]
@@ -32,6 +39,8 @@ class ZinvoltData:
class ZinvoltDeviceCoordinator(DataUpdateCoordinator[ZinvoltData]):
"""Class for Zinvolt devices."""
battery_units: dict[str, Unit]
def __init__(
self,
hass: HomeAssistant,
@@ -50,15 +59,30 @@ class ZinvoltDeviceCoordinator(DataUpdateCoordinator[ZinvoltData]):
self.battery = battery
self.client = client
async def _async_setup(self) -> None:
"""Set up the Zinvolt integration."""
try:
units = await self.client.get_units(self.battery.identifier)
except ZinvoltError as err:
raise UpdateFailed(
translation_key="update_failed", translation_domain=DOMAIN
) from err
self.battery_units = {
unit.serial_number: unit for unit in units if unit.type is UnitType.BATTERY
}
async def _async_update_data(self) -> ZinvoltData:
"""Update data from Zinvolt."""
try:
battery_state = await self.client.get_battery_status(
self.battery.identifier
)
battery_unit = await self.client.get_battery_unit(
self.battery.identifier, self.battery.serial_number
)
battery_units = {
unit_serial_number: await self.client.get_battery_unit(
self.battery.identifier, unit_serial_number
)
for unit_serial_number in self.battery_units
}
except ZinvoltError as err:
raise UpdateFailed(
translation_key="update_failed",
@@ -66,7 +90,15 @@ class ZinvoltDeviceCoordinator(DataUpdateCoordinator[ZinvoltData]):
) from err
return ZinvoltData(
battery_state,
battery_unit.version.current_version,
battery_unit.battery_model,
{point.point.lower(): point.normal for point in battery_unit.points},
{
serial_number: BatteryData(
battery_unit.version.current_version,
battery_unit.battery_model,
{
point.point.lower(): point.normal
for point in battery_unit.points
},
)
for serial_number, battery_unit in battery_units.items()
},
)

View File

@@ -1,10 +1,13 @@
"""Base entity for Zinvolt integration."""
from zinvolt.models import Unit
from homeassistant.const import ATTR_VIA_DEVICE
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import ZinvoltDeviceCoordinator
from .coordinator import BatteryData, ZinvoltDeviceCoordinator
class ZinvoltEntity(CoordinatorEntity[ZinvoltDeviceCoordinator]):
@@ -20,6 +23,55 @@ class ZinvoltEntity(CoordinatorEntity[ZinvoltDeviceCoordinator]):
manufacturer="Zinvolt",
name=coordinator.battery.name,
serial_number=coordinator.data.battery.serial_number,
model_id=coordinator.data.model,
sw_version=coordinator.data.sw_version,
)
class ZinvoltUnitEntity(ZinvoltEntity):
"""Base entity for Zinvolt units."""
def __init__(
self, coordinator: ZinvoltDeviceCoordinator, unit_serial_number: str
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self.unit_serial_number = unit_serial_number
is_main_device = (
list(coordinator.battery_units).index(self.unit_serial_number) == 0
)
self.serial_number = (
coordinator.data.battery.serial_number
if is_main_device
else self.battery_unit.serial_number
)
name = coordinator.battery.name if is_main_device else self.battery_unit.name
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.serial_number)},
manufacturer="Zinvolt",
name=name,
serial_number=self.serial_number,
sw_version=self.battery_unit.version.current_version,
model_id=self.battery.model,
)
if not is_main_device:
self._attr_device_info[ATTR_VIA_DEVICE] = (
DOMAIN,
coordinator.data.battery.serial_number,
)
@property
def battery(self) -> BatteryData:
"""Return the battery data."""
return self.coordinator.data.batteries[self.unit_serial_number]
@property
def battery_unit(self) -> Unit:
"""Return the battery unit."""
return self.coordinator.battery_units[self.unit_serial_number]
@property
def available(self) -> bool:
"""Return if the entity is available."""
return (
super().available
and self.unit_serial_number in self.coordinator.data.batteries
)

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["zinvolt"],
"quality_scale": "bronze",
"requirements": ["zinvolt==0.3.0"]
"requirements": ["zinvolt==0.4.1"]
}

View File

@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)

View File

@@ -6,7 +6,7 @@ from functools import cache
from getpass import getuser
import logging
import platform
from typing import TYPE_CHECKING, Any
from typing import Any
from homeassistant.const import __version__ as current_version
from homeassistant.core import HomeAssistant
@@ -15,7 +15,6 @@ from homeassistant.util.package import is_docker_env, is_virtual_env
from homeassistant.util.system_info import is_official_image
from .hassio import is_hassio
from .importlib import async_import_module
from .singleton import singleton
_LOGGER = logging.getLogger(__name__)
@@ -54,15 +53,6 @@ cached_get_user = cache(getuser)
@bind_hass
async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]:
"""Return info about the system."""
# Local import to avoid circular dependencies
# We use the import helper because hassio
# may not be loaded yet and we don't want to
# do blocking I/O in the event loop to import it.
if TYPE_CHECKING:
from homeassistant.components import hassio # noqa: PLC0415
else:
hassio = await async_import_module(hass, "homeassistant.components.hassio")
is_hassio_ = is_hassio(hass)
info_object = {
@@ -105,6 +95,9 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]:
# Enrich with Supervisor information
if is_hassio_:
# Local import to avoid circular dependencies
from homeassistant.components import hassio # noqa: PLC0415
if not (info := hassio.get_info(hass)):
_LOGGER.warning("No Home Assistant Supervisor info available")
info = {}

View File

@@ -39,7 +39,7 @@ habluetooth==5.11.1
hass-nabucasa==2.2.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260325.5
home-assistant-frontend==20260325.6
home-assistant-intents==2026.3.24
httpx==0.28.1
ifaddr==0.2.0

View File

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

16
requirements_all.txt generated
View File

@@ -533,7 +533,7 @@ aqualogic==2.6
aranet4==2.6.0
# homeassistant.components.arcam_fmj
arcam-fmj==1.8.2
arcam-fmj==1.8.3
# homeassistant.components.arris_tg2492lg
arris-tg2492lg==2.2.0
@@ -1226,10 +1226,10 @@ hole==0.9.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.84
holidays==0.93
# homeassistant.components.frontend
home-assistant-frontend==20260325.5
home-assistant-frontend==20260325.6
# homeassistant.components.conversation
home-assistant-intents==2026.3.24
@@ -1832,7 +1832,7 @@ proxmoxer==2.3.0
psutil-home-assistant==0.0.1
# homeassistant.components.systemmonitor
psutil==7.1.2
psutil==7.2.2
# homeassistant.components.pulseaudio_loopback
pulsectl==23.5.2
@@ -2500,13 +2500,13 @@ pysmappee==0.2.29
pysmarlaapi==1.0.2
# homeassistant.components.smartthings
pysmartthings==3.7.2
pysmartthings==3.7.3
# homeassistant.components.smarty
pysmarty2==0.10.3
# homeassistant.components.smhi
pysmhi==1.1.0
pysmhi==2.0.0
# homeassistant.components.edl21
pysml==0.1.5
@@ -2972,7 +2972,7 @@ smart-meter-texas==0.5.5
snapcast==2.3.7
# homeassistant.components.sonos
soco==0.30.14
soco==0.30.15
# homeassistant.components.solaredge_local
solaredge-local==0.2.3
@@ -3392,7 +3392,7 @@ zhong-hong-hvac==1.0.13
ziggo-mediabox-xl==1.1.0
# homeassistant.components.zinvolt
zinvolt==0.3.0
zinvolt==0.4.1
# homeassistant.components.zoneminder
zm-py==0.5.4

View File

@@ -44,7 +44,7 @@ types-chardet==0.1.5
types-decorator==5.2.0.20251101
types-pexpect==4.9.0.20250916
types-protobuf==6.30.2.20250914
types-psutil==7.1.1.20251122
types-psutil==7.2.2.20260402
types-pyserial==3.5.0.20251001
types-python-dateutil==2.9.0.20260124
types-python-slugify==8.0.2.20240310

View File

@@ -506,7 +506,7 @@ apsystems-ez1==2.7.0
aranet4==2.6.0
# homeassistant.components.arcam_fmj
arcam-fmj==1.8.2
arcam-fmj==1.8.3
# homeassistant.components.asuswrt
asusrouter==1.21.3
@@ -1090,10 +1090,10 @@ hole==0.9.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.84
holidays==0.93
# homeassistant.components.frontend
home-assistant-frontend==20260325.5
home-assistant-frontend==20260325.6
# homeassistant.components.conversation
home-assistant-intents==2026.3.24
@@ -1590,7 +1590,7 @@ proxmoxer==2.3.0
psutil-home-assistant==0.0.1
# homeassistant.components.systemmonitor
psutil==7.1.2
psutil==7.2.2
# homeassistant.components.pushbullet
pushbullet.py==0.11.0
@@ -2135,13 +2135,13 @@ pysmappee==0.2.29
pysmarlaapi==1.0.2
# homeassistant.components.smartthings
pysmartthings==3.7.2
pysmartthings==3.7.3
# homeassistant.components.smarty
pysmarty2==0.10.3
# homeassistant.components.smhi
pysmhi==1.1.0
pysmhi==2.0.0
# homeassistant.components.edl21
pysml==0.1.5
@@ -2517,7 +2517,7 @@ smart-meter-texas==0.5.5
snapcast==2.3.7
# homeassistant.components.sonos
soco==0.30.14
soco==0.30.15
# homeassistant.components.solaredge
solaredge-web==0.0.1
@@ -2868,7 +2868,7 @@ zeversolar==0.3.2
zha==1.1.1
# homeassistant.components.zinvolt
zinvolt==0.3.0
zinvolt==0.4.1
# homeassistant.components.zoneminder
zm-py==0.5.4

View File

@@ -13,9 +13,11 @@ from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.util import slugify
from .const import MOCK_FB_SERVICES, MOCK_USER_DATA
from .const import MOCK_FB_SERVICES, MOCK_MESH_MASTER_MAC, MOCK_USER_DATA
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import ClientSessionGenerator
@@ -126,7 +128,7 @@ async def test_image_entity(
}
assert (state := entity_registry.async_get("image.mock_title_guestwifi"))
assert state.unique_id == "1c_ed_6f_12_34_11_guestwifi_qr_code"
assert state.unique_id == "1C:ED:6F:12:34:11-guest_wifi_qr_code"
# test image download
client = await hass_client()
@@ -222,3 +224,53 @@ async def test_image_update_unavailable(
assert (state := hass.states.get(entity_id))
assert state.state != STATE_UNKNOWN
async def test_migrate_to_new_unique_id(
hass: HomeAssistant,
fc_class_mock,
fh_class_mock,
entity_registry: EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test migrate from old unique id to new unique id."""
mock_unique_id = "1234567890"
entry = MockConfigEntry(
domain=DOMAIN,
data=MOCK_USER_DATA,
unique_id=mock_unique_id,
)
entry.add_to_hass(hass)
old_unique_id = slugify(f"{MOCK_MESH_MASTER_MAC}-MyWifi-qr-code")
new_unique_id = f"{MOCK_MESH_MASTER_MAC}-guest_wifi_qr_code"
entity_registry.async_get_or_create(
suggested_object_id="mock_title_mywifi",
disabled_by=None,
domain=IMAGE_DOMAIN,
platform=DOMAIN,
unique_id=old_unique_id,
config_entry=entry,
)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, mock_unique_id)},
connections={(dr.CONNECTION_NETWORK_MAC, MOCK_MESH_MASTER_MAC)},
)
await hass.async_block_till_done()
entity_entry = entity_registry.async_get("image.mock_title_mywifi")
assert entity_entry
assert entity_entry.unique_id == old_unique_id
with patch("homeassistant.components.fritz.PLATFORMS", [Platform.IMAGE]):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_entry = entity_registry.async_get("image.mock_title_mywifi")
assert entity_entry
assert entity_entry.unique_id == new_unique_id

View File

@@ -66,15 +66,15 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_COUNTRY: "SE",
CONF_COUNTRY: "AL",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "Sweden"
assert result2["title"] == "Albania"
assert result2["data"] == {
"country": "SE",
"country": "AL",
}
@@ -90,12 +90,12 @@ async def test_form_translated_title(hass: HomeAssistant) -> None:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_COUNTRY: "SE",
CONF_COUNTRY: "AL",
},
)
await hass.async_block_till_done()
assert result2["title"] == "Schweden"
assert result2["title"] == "Albanien"
@pytest.mark.usefixtures("mock_setup_entry")
@@ -105,20 +105,20 @@ async def test_single_combination_country_province(hass: HomeAssistant) -> None:
CONF_COUNTRY: "DE",
CONF_PROVINCE: "BW",
}
data_se = {
CONF_COUNTRY: "SE",
data_al = {
CONF_COUNTRY: "AL",
}
MockConfigEntry(domain=DOMAIN, data=data_de).add_to_hass(hass)
MockConfigEntry(domain=DOMAIN, data=data_se).add_to_hass(hass)
MockConfigEntry(domain=DOMAIN, data=data_al).add_to_hass(hass)
# Test for country without subdivisions
result_se = await hass.config_entries.flow.async_init(
result_al = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=data_se,
data=data_al,
)
assert result_se["type"] is FlowResultType.ABORT
assert result_se["reason"] == "already_configured"
assert result_al["type"] is FlowResultType.ABORT
assert result_al["reason"] == "already_configured"
# Test for country with subdivisions
result_de_step1 = await hass.config_entries.flow.async_init(
@@ -150,12 +150,12 @@ async def test_form_babel_unresolved_language(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_COUNTRY: "SE",
CONF_COUNTRY: "AL",
},
)
await hass.async_block_till_done()
assert result["title"] == "Sweden"
assert result["title"] == "Albania"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -197,12 +197,12 @@ async def test_form_babel_replace_dash_with_underscore(hass: HomeAssistant) -> N
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_COUNTRY: "SE",
CONF_COUNTRY: "AL",
},
)
await hass.async_block_till_done()
assert result["title"] == "Sweden"
assert result["title"] == "Albania"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -430,8 +430,8 @@ async def test_options_abort_no_categories(hass: HomeAssistant) -> None:
"""Test the options flow abort if no categories to select."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_COUNTRY: "SE"},
title="Sweden",
data={CONF_COUNTRY: "AL"},
title="Albania",
)
config_entry.add_to_hass(hass)

View File

@@ -226,6 +226,20 @@ async def test_update_from_water_heater(
assert state
assert state.state == STATE_ECO
# confirm water heater state is 'off' when SystemMode is set to 0 (kOff)
set_node_attribute(matter_node, 2, 513, 28, 0)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
# confirm water heater state returns to 'eco' when SystemMode is set back to 4 (kHeat)
set_node_attribute(matter_node, 2, 513, 28, 4)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_ECO
@pytest.mark.parametrize("node_fixture", ["silabs_water_heater"])
async def test_water_heater_turn_on_off(

View File

@@ -149,7 +149,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.pve1_status-entry]

View File

@@ -294,10 +294,32 @@ async def test_camera_image(
await setup_platform(hass, Platform.CAMERA)
front_camera_mock = mock_ring_devices.get_device(765432)
front_camera_mock.async_get_snapshot.return_value = SMALLEST_VALID_JPEG_BYTES
state = hass.states.get("camera.front_live_view")
assert state is not None
# For live_view camera, snapshot should use async_get_snapshot
image = await async_get_image(hass, "camera.front_live_view")
assert image.content == SMALLEST_VALID_JPEG_BYTES
front_camera_mock.async_get_snapshot.assert_called_once()
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_camera_last_recording_image(
hass: HomeAssistant,
mock_ring_client,
mock_ring_devices,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test last recording camera will return still image from video when available."""
await setup_platform(hass, Platform.CAMERA)
front_camera_mock = mock_ring_devices.get_device(765432)
state = hass.states.get("camera.front_last_recording")
assert state is not None
# history not updated yet
front_camera_mock.async_history.assert_not_called()
front_camera_mock.async_recording_url.assert_not_called()
@@ -308,55 +330,23 @@ async def test_camera_image(
),
pytest.raises(HomeAssistantError),
):
image = await async_get_image(hass, "camera.front_live_view")
await async_get_image(hass, "camera.front_last_recording")
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# history updated so image available
front_camera_mock.async_history.assert_called_once()
front_camera_mock.async_recording_url.assert_called_once()
assert front_camera_mock.async_recording_url.call_count == 2
with patch(
"homeassistant.components.ring.camera.ffmpeg.async_get_image",
return_value=SMALLEST_VALID_JPEG_BYTES,
):
image = await async_get_image(hass, "camera.front_live_view")
image = await async_get_image(hass, "camera.front_last_recording")
assert image.content == SMALLEST_VALID_JPEG_BYTES
async def test_camera_live_view_no_subscription(
hass: HomeAssistant,
mock_ring_client,
mock_ring_devices,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test live view camera skips recording URL when no subscription."""
await setup_platform(hass, Platform.CAMERA)
front_camera_mock = mock_ring_devices.get_device(765432)
# Set device to not have subscription
front_camera_mock.has_subscription = False
state = hass.states.get("camera.front_live_view")
assert state is not None
# Reset mock call counts
front_camera_mock.async_recording_url.reset_mock()
# Trigger coordinator update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# For cameras without subscription, recording URL should NOT be fetched
front_camera_mock.async_recording_url.assert_not_called()
# Requesting an image without subscription should raise an error
with pytest.raises(HomeAssistantError):
await async_get_image(hass, "camera.front_live_view")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_camera_stream_attributes(
hass: HomeAssistant,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,148 +1,37 @@
{
"approvedTime": "2023-08-07T07:07:34Z",
"referenceTime": "2023-08-07T07:00:00Z",
"geometry": {
"type": "Point",
"coordinates": [[15.990068, 57.997072]]
},
"createdTime": "2026-04-02T11:01:32Z",
"referenceTime": "2026-04-02T10:45:00Z",
"geometry": { "type": "Point", "coordinates": [16.158549, 58.577821] },
"timeSeries": [
{
"validTime": "2023-08-07T08:00:00Z",
"parameters": [
{
"name": "spp",
"levelType": "hl",
"level": 0,
"unit": "percent",
"values": [-9]
},
{
"name": "pcat",
"levelType": "hl",
"level": 0,
"unit": "category",
"values": [0]
},
{
"name": "pmin",
"levelType": "hl",
"level": 0,
"unit": "kg/m2/h",
"values": [0.0]
},
{
"name": "pmean",
"levelType": "hl",
"level": 0,
"unit": "kg/m2/h",
"values": [0.0]
},
{
"name": "pmax",
"levelType": "hl",
"level": 0,
"unit": "kg/m2/h",
"values": [0.0]
},
{
"name": "pmedian",
"levelType": "hl",
"level": 0,
"unit": "kg/m2/h",
"values": [0.0]
},
{
"name": "tcc_mean",
"levelType": "hl",
"level": 0,
"unit": "octas",
"values": [8]
},
{
"name": "lcc_mean",
"levelType": "hl",
"level": 0,
"unit": "octas",
"values": [8]
},
{
"name": "mcc_mean",
"levelType": "hl",
"level": 0,
"unit": "octas",
"values": [7]
},
{
"name": "hcc_mean",
"levelType": "hl",
"level": 0,
"unit": "octas",
"values": [7]
},
{
"name": "t",
"levelType": "hl",
"level": 2,
"unit": "Cel",
"values": [18.4]
},
{
"name": "msl",
"levelType": "hmsl",
"level": 0,
"unit": "hPa",
"values": [992.4]
},
{
"name": "vis",
"levelType": "hl",
"level": 2,
"unit": "km",
"values": [0.4]
},
{
"name": "wd",
"levelType": "hl",
"level": 10,
"unit": "degree",
"values": [93]
},
{
"name": "ws",
"levelType": "hl",
"level": 10,
"unit": "m/s",
"values": [2.5]
},
{
"name": "r",
"levelType": "hl",
"level": 2,
"unit": "percent",
"values": [100]
},
{
"name": "tstm",
"levelType": "hl",
"level": 0,
"unit": "percent",
"values": [37]
},
{
"name": "gust",
"levelType": "hl",
"level": 10,
"unit": "m/s",
"values": [6.2]
},
{
"name": "Wsymb2",
"levelType": "hl",
"level": 0,
"unit": "category",
"values": [7]
}
]
"time": "2026-04-02T11:00:00Z",
"intervalParametersStartTime": "2026-04-02T10:00:00Z",
"data": {
"air_temperature": 10.4,
"wind_from_direction": 255,
"wind_speed": 3.4,
"wind_speed_of_gust": 6.8,
"relative_humidity": 80,
"air_pressure_at_mean_sea_level": 1011.5,
"visibility_in_air": 17.9,
"thunderstorm_probability": 0,
"probability_of_frozen_precipitation": 0.0,
"cloud_area_fraction": 8,
"low_type_cloud_area_fraction": 8,
"medium_type_cloud_area_fraction": 8,
"high_type_cloud_area_fraction": 2,
"cloud_base_altitude": 528,
"cloud_top_altitude": 2250,
"precipitation_amount_mean_deterministic": 0.0,
"precipitation_amount_mean": 0.0,
"precipitation_amount_min": 0.1,
"precipitation_amount_max": 0.3,
"precipitation_amount_median": 0.0,
"probability_of_precipitation": 10,
"precipitation_frozen_part": 0,
"predominant_precipitation_type_at_surface": 1,
"symbol_code": 6
}
}
]
}

View File

@@ -508,7 +508,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '88',
'state': '25',
})
# ---
# name: test_sensor_setup[load_platforms0][sensor.test_highest_grass_fire_risk-entry]
@@ -735,7 +735,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '88',
'state': '100',
})
# ---
# name: test_sensor_setup[load_platforms0][sensor.test_potential_rate_of_spread-entry]
@@ -805,13 +805,19 @@
'area_id': None,
'capabilities': dict({
'options': list([
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'no_precipitation',
'rain',
'thunderstorm',
'freezing_rain',
'mixed_ice',
'snow',
'wet_snow',
'rain_snow_mixed',
'ice_pellets',
'graupel',
'hail',
'drizzle',
'freezing_drizzle',
]),
}),
'config_entry_id': <ANY>,
@@ -851,13 +857,19 @@
'device_class': 'enum',
'friendly_name': 'Test Precipitation category',
'options': list([
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'no_precipitation',
'rain',
'thunderstorm',
'freezing_rain',
'mixed_ice',
'snow',
'wet_snow',
'rain_snow_mixed',
'ice_pellets',
'graupel',
'hail',
'drizzle',
'freezing_drizzle',
]),
}),
'context': <ANY>,
@@ -865,7 +877,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
'state': 'rain',
})
# ---
# name: test_sensor_setup[load_platforms0][sensor.test_thunder_probability-entry]
@@ -917,7 +929,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '37',
'state': '0',
})
# ---
# name: test_sensor_setup[load_platforms0][sensor.test_total_cloud_coverage-entry]

File diff suppressed because it is too large Load Diff

View File

@@ -57,7 +57,7 @@ async def test_setup_hass(
state = hass.states.get(ENTITY_ID)
assert state
assert state.state == "fog"
assert state.state == "cloudy"
assert state.attributes == snapshot
@@ -65,7 +65,7 @@ async def test_setup_hass(
"to_load",
[1],
)
@pytest.mark.freeze_time(datetime(2023, 8, 7, 1, tzinfo=dt_util.UTC))
@pytest.mark.freeze_time(datetime(2026, 4, 3, 1, tzinfo=dt_util.UTC))
async def test_clear_night(
hass: HomeAssistant,
mock_client: SMHIPointForecast,
@@ -134,7 +134,7 @@ async def test_properties_no_data(
assert state
assert state.name == "Test"
assert state.state == "fog"
assert state.state == "cloudy"
assert ATTR_SMHI_THUNDER_PROBABILITY not in state.attributes
assert state.attributes[ATTR_ATTRIBUTION] == "Swedish weather institute (SMHI)"
@@ -356,7 +356,7 @@ async def test_custom_speed_unit(
assert state
assert state.name == "Test"
assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 22.32
assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 24.48
entity_registry.async_update_entity_options(
state.entity_id,
@@ -367,7 +367,7 @@ async def test_custom_speed_unit(
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 6.2
assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 6.8
@pytest.mark.parametrize(
@@ -400,7 +400,7 @@ async def test_forecast_services(
assert msg["type"] == "event"
forecast1 = msg["event"]["forecast"]
assert len(forecast1) == 10
assert len(forecast1) == 11
assert forecast1[0] == snapshot
assert forecast1[6] == snapshot
@@ -421,7 +421,7 @@ async def test_forecast_services(
assert msg["type"] == "event"
forecast1 = msg["event"]["forecast"]
assert len(forecast1) == 52
assert len(forecast1) == 59
assert forecast1[0] == snapshot
assert forecast1[6] == snapshot

View File

@@ -1175,6 +1175,46 @@ async def test_media_transport(
assert getattr(soco, client_call).call_count == 1
@pytest.mark.parametrize(
("transport_state", "title", "expected_state"),
[
("PAUSED_PLAYBACK", "Something", "paused"),
("PAUSED_PLAYBACK", None, "idle"),
("PAUSED_PLAYBACK", " ", "idle"),
("STOPPED", "Something", "paused"),
("STOPPED", None, "idle"),
("STOPPED", " ", "idle"),
],
)
async def test_state_paused_idle(
hass: HomeAssistant,
soco: MockSoCo,
async_autosetup_sonos,
no_media_event: SonosMockEvent,
transport_state: str,
title: str | None,
expected_state: str,
) -> None:
"""Test that idle is returned when title is None or whitespace, paused otherwise."""
soco.get_current_track_info.return_value = {
"title": title,
"artist": "",
"album": "",
"album_art": "",
"position": "NOT_IMPLEMENTED",
"playlist_position": "1",
"duration": "NOT_IMPLEMENTED",
"uri": "x-file-cifs://192.168.42.10/music/track.mp3",
"metadata": "NOT_IMPLEMENTED",
}
no_media_event.variables["transport_state"] = transport_state
soco.avTransport.subscribe.return_value.callback(no_media_event)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get("media_player.zone_a")
assert state.state == expected_state
async def test_play_media_announce(
hass: HomeAssistant,
soco: MockSoCo,

View File

@@ -21,6 +21,7 @@ from homeassistant.components.sonos.switch import (
ATTR_SPEECH_ENHANCEMENT_ENABLED,
ATTR_VOLUME,
)
from homeassistant.components.ssdp import SsdpChange
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import (
@@ -33,6 +34,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_UDN, SsdpServiceInfo
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@@ -337,3 +339,39 @@ async def test_alarm_change_device(
alarm_14 = entity_registry.async_get(entity_id)
device = device_registry.async_get(alarm_14.device_id)
assert device.name == soco_br.get_speaker_info()["zone_name"]
async def test_alarm_setup_for_undiscovered_speaker(
hass: HomeAssistant,
async_setup_sonos,
alarm_clock,
entity_registry: er.EntityRegistry,
soco_factory: SoCoMockFactory,
discover,
) -> None:
"""Test for creation of alarm on a speaker that is discovered after the integration is setup."""
soco_bedroom = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom")
one_alarm = copy(alarm_clock.ListAlarms.return_value)
one_alarm["CurrentAlarmList"] = one_alarm["CurrentAlarmList"].replace(
"RINCON_test", soco_bedroom.uid
)
alarm_clock.ListAlarms.return_value = one_alarm
await async_setup_sonos()
# Switch should not be created since the speaker isn't discovered yet
assert "switch.sonos_alarm_14" not in entity_registry.entities
# Simulate discovery of the bedroom speaker
discover.call_args.args[1](
SsdpServiceInfo(
ssdp_location=f"http://{soco_bedroom.ip_address}/",
ssdp_st="urn:schemas-upnp-org:device:ZonePlayer:1",
ssdp_usn=f"uuid:{soco_bedroom.uid}_MR::urn:schemas-upnp-org:service:GroupRenderingControl:1",
upnp={ATTR_UPNP_UDN: f"uuid:{soco_bedroom.uid}"},
),
SsdpChange.ALIVE,
)
await hass.async_block_till_done(wait_background_tasks=True)
assert "switch.sonos_alarm_14" in entity_registry.entities

View File

@@ -7,7 +7,7 @@ import socket
from unittest.mock import AsyncMock, Mock, NonCallableMock, patch
from psutil import NoSuchProcess, Process
from psutil._common import (
from psutil._ntuples import (
sbattery,
sdiskpart,
sdiskusage,

View File

@@ -5,7 +5,7 @@ import socket
from unittest.mock import Mock, patch
from freezegun.api import FrozenDateTimeFactory
from psutil._common import sdiskpart, sdiskusage, shwtemp, snetio, snicaddr
from psutil._ntuples import sdiskpart, sdiskusage, shwtemp, snetio, snicaddr
import pytest
from syrupy.assertion import SnapshotAssertion

View File

@@ -2,7 +2,7 @@
from unittest.mock import Mock
from psutil._common import sdiskpart
from psutil._ntuples import sdiskpart
import pytest
from homeassistant.core import HomeAssistant

View File

@@ -233,6 +233,7 @@ async def test_full_flow_with_domain_registration(
assert parsed_query["client_id"][0] == "user_client_id"
assert parsed_query["redirect_uri"][0] == REDIRECT
assert parsed_query["state"][0] == state
assert parsed_query["prompt_missing_scopes"][0] == "true"
assert parsed_query["scope"][0] == " ".join(SCOPES)
assert "code_challenge" not in parsed_query

View File

@@ -1,52 +0,0 @@
# serializer version: 1
# name: test_binary_sensors[binary_sensor.transmission_port_forwarding-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.transmission_port_forwarding',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Port forwarding',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Port forwarding',
'platform': 'transmission',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'port_forwarding',
'unique_id': '01J0BC4QM2YBRP6H5G933AETT7-port_forwarding',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.transmission_port_forwarding-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'Transmission Port forwarding',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.transmission_port_forwarding',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@@ -1,31 +0,0 @@
"""Tests for the Transmission binary sensor platform."""
from unittest.mock import AsyncMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_binary_sensors(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_transmission_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the binary sensor entities."""
with patch(
"homeassistant.components.transmission.PLATFORMS", [Platform.BINARY_SENSOR]
):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)

View File

@@ -144,8 +144,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -154,14 +160,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.pykascx9yfqrxtbgzcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.3dprinter_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': '3DPrinter Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.3dprinter_total_energy',
@@ -380,6 +388,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -795,8 +806,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -805,15 +822,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.ZDldMHS0tjmQgGxEzcadd_ele',
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.ak1_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'AK1 Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ak1_total_energy',
@@ -1640,8 +1658,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -1650,14 +1674,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.cju47ovcbeuapei2zcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.aubess_cooker_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Aubess Cooker Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.aubess_cooker_total_energy',
@@ -1873,8 +1899,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -1883,14 +1915,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.zjh9xhtm3gibs9kizcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.aubess_washing_machine_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Aubess Washing Machine Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.aubess_washing_machine_total_energy',
@@ -2445,6 +2479,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -5000,6 +5037,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -5345,8 +5385,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -5355,14 +5401,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.jgsopsvzh2ec3itjzcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.dehumidifier_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Dehumidifier Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.dehumidifier_total_energy',
@@ -5851,8 +5899,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -5861,15 +5915,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.l8uxezzkc7c5a0jhzcadd_ele',
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.droger_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'droger Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.droger_total_energy',
@@ -7213,8 +7268,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -7223,14 +7284,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.cq4hzlrnqn4qi0mqzcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Elivco Kitchen Socket Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.elivco_kitchen_socket_total_energy',
@@ -7446,8 +7509,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -7456,14 +7525,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.pz2xuth8hczv6zrwzcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.elivco_tv_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Elivco TV Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.elivco_tv_total_energy',
@@ -7734,8 +7805,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -7744,14 +7821,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.51tdkcsamisw9ukycpadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.framboisier_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Framboisier Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.framboisier_total_energy',
@@ -8246,6 +8325,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -9158,8 +9240,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -9168,14 +9256,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.sxa4ealyi9cotiugzcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.ha_socket_delta_test_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'HA Socket Delta Test Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ha_socket_delta_test_total_energy',
@@ -10221,6 +10311,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -10619,8 +10712,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -10629,14 +10728,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.vx2owjsg86g2ys93zcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.ineox_sp2_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Ineox SP2 Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ineox_sp2_total_energy',
@@ -10964,6 +11065,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -11738,8 +11842,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -11748,15 +11858,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.g7af6lrt4miugbstcpadd_ele',
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.keller_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Keller Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.keller_total_energy',
@@ -12079,6 +12190,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -12314,8 +12428,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -12324,15 +12444,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.uvh6oeqrfliovfiwzcadd_ele',
'unit_of_measurement': '度',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.licht_drucker_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Licht drucker Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': '度',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.licht_drucker_total_energy',
@@ -15153,8 +15274,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -15163,15 +15290,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.uc9fL2NpR79iCzGIzcadd_ele',
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.n4_auto_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'N4-Auto Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.n4_auto_total_energy',
@@ -15555,8 +15683,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -15565,14 +15699,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.2x473nefusdo7af6zcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.office_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Office Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.office_total_energy',
@@ -17882,6 +18018,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -19716,8 +19855,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -19726,14 +19871,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.4q5c2am8n1bwb6bszcadd_ele',
'unit_of_measurement': None,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.socket4_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Socket4 Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.socket4_total_energy',
@@ -20393,6 +20540,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -22851,6 +23001,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
@@ -23651,8 +23804,14 @@
'name': None,
'object_id_base': 'Total energy',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Total energy',
'platform': 'tuya',
@@ -23661,15 +23820,16 @@
'supported_features': 0,
'translation_key': 'total_energy',
'unique_id': 'tuya.dNBnmtjLU8eRWHf0zcadd_ele',
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_platform_setup_and_discovery[sensor.weihnachten3_total_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Weihnachten3 Total energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': '',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.weihnachten3_total_energy',
@@ -23888,6 +24048,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,

View File

@@ -48,6 +48,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.setup import async_setup_component
from .common import MockUpdateEntity
from tests.common import (
MockConfigEntry,
MockEntityPlatform,
@@ -64,13 +66,9 @@ from tests.typing import WebSocketGenerator
TEST_DOMAIN = "test"
class MockUpdateEntity(UpdateEntity):
"""Mock UpdateEntity to use in tests."""
async def test_update(hass: HomeAssistant) -> None:
"""Test getting data from the mocked update entity."""
update = MockUpdateEntity()
update = UpdateEntity()
update.hass = hass
update.platform = MockEntityPlatform(hass)
@@ -797,6 +795,41 @@ async def test_release_notes_entity_does_not_support_release_notes(
assert result["error"]["message"] == "Entity does not support release notes"
async def test_release_notes_entity_unavailable(
hass: HomeAssistant,
mock_update_entities: list[MockUpdateEntity],
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test getting the release notes for entity that is unavailable."""
entity = MockUpdateEntity(
name="Update unavailable",
unique_id="unavailable",
installed_version="1.0.0",
latest_version="1.0.1",
available=False,
supported_features=UpdateEntityFeature.RELEASE_NOTES,
)
setup_test_component_platform(hass, DOMAIN, [entity])
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await hass.async_block_till_done()
await client.send_json(
{
"id": 1,
"type": "update/release_notes",
"entity_id": "update.update_unavailable",
}
)
result = await client.receive_json()
assert result["error"]["code"] == "home_assistant_error"
assert result["error"]["message"] == "Entity is not available"
class MockFlow(ConfigFlow):
"""Test flow."""

View File

@@ -80,7 +80,6 @@ async def test_state_machine_updates_from_device_callbacks(
| MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.GROUPING
| MediaPlayerEntityFeature.SEEK
)
@@ -130,7 +129,6 @@ async def test_state_machine_updates_from_device_callbacks(
| MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.GROUPING
| MediaPlayerEntityFeature.SEEK
| MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.REPEAT_SET

View File

@@ -4,7 +4,7 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from zinvolt.models import BatteryListResponse, BatteryState, BatteryUnit
from zinvolt.models import BatteryListResponse, BatteryState, BatteryUnit, UnitsResponse
from homeassistant.components.zinvolt.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN
@@ -57,4 +57,7 @@ def mock_zinvolt_client() -> Generator[AsyncMock]:
client.get_battery_unit.return_value = BatteryUnit.from_json(
load_fixture("battery_unit.json", DOMAIN)
)
client.get_units.return_value = UnitsResponse.from_json(
load_fixture("units.json", DOMAIN)
).units
yield client

View File

@@ -0,0 +1,48 @@
{
"units": [
{
"usn": "INV001",
"name": "Inverter",
"type": "INVERTER",
"abnormalAmount": 0,
"resettable": false,
"version": {
"currentVersion": "V1.032",
"status": "NO_UPDATE"
}
},
{
"usn": "ems",
"name": "EMS",
"type": "EMS",
"abnormalAmount": 0,
"resettable": false,
"version": {
"currentVersion": "V1.01.45E",
"status": "NO_UPDATE"
}
},
{
"usn": "BAT001",
"name": "Battery - 1",
"type": "BATTERY",
"abnormalAmount": 0,
"resettable": false,
"version": {
"currentVersion": "V1.20",
"status": "NO_UPDATE"
}
},
{
"usn": "BAT002",
"name": "Battery - 2",
"type": "BATTERY",
"abnormalAmount": 0,
"resettable": false,
"version": {
"currentVersion": "V1.20",
"status": "NO_UPDATE"
}
}
]
}

View File

@@ -1,4 +1,361 @@
# serializer version: 1
# name: test_all_entities[binary_sensor.battery_2_charge-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.battery_2_charge',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Charge',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Charge',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charge',
'unique_id': 'BAT002.charge',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.battery_2_charge-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Battery - 2 Charge',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.battery_2_charge',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.battery_2_communication-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.battery_2_communication',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Communication',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Communication',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'communication',
'unique_id': 'BAT002.communication',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.battery_2_communication-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Battery - 2 Communication',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.battery_2_communication',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.battery_2_current-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.battery_2_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Current',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Current',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'current',
'unique_id': 'BAT002.current',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.battery_2_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Battery - 2 Current',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.battery_2_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.battery_2_discharge-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.battery_2_discharge',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Discharge',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Discharge',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'discharge',
'unique_id': 'BAT002.discharge',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.battery_2_discharge-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Battery - 2 Discharge',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.battery_2_discharge',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.battery_2_heat-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.battery_2_heat',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Heat',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.HEAT: 'heat'>,
'original_icon': None,
'original_name': 'Heat',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': 'BAT002.temperature',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.battery_2_heat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'heat',
'friendly_name': 'Battery - 2 Heat',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.battery_2_heat',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.battery_2_other_problems-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.battery_2_other_problems',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Other problems',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Other problems',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'other',
'unique_id': 'BAT002.other',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.battery_2_other_problems-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Battery - 2 Other problems',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.battery_2_other_problems',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.battery_2_voltage-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.battery_2_voltage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Voltage',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Voltage',
'platform': 'zinvolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'voltage',
'unique_id': 'BAT002.voltage',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.battery_2_voltage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Battery - 2 Voltage',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.battery_2_voltage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.zinvolt_batterij_charge-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -4,6 +4,34 @@
'coordinators': list([
dict({
'a125ef17-6bdf-45ad-b106-ce54e95e4634': dict({
'batteries': dict({
'BAT001': dict({
'model': 'ZVS4000',
'points': dict({
'charge': True,
'communication': True,
'current': True,
'discharge': True,
'other': True,
'temperature': True,
'voltage': True,
}),
'sw_version': 'V1.02-V0.00.000',
}),
'BAT002': dict({
'model': 'ZVS4000',
'points': dict({
'charge': True,
'communication': True,
'current': True,
'discharge': True,
'other': True,
'temperature': True,
'voltage': True,
}),
'sw_version': 'V1.02-V0.00.000',
}),
}),
'battery': dict({
'current_power': dict({
'is_dormant': False,
@@ -29,17 +57,6 @@
'serial_number': 'ZVG011025120088',
'smart_mode': 'CHARGED',
}),
'model': 'ZVS4000',
'points': dict({
'charge': True,
'communication': True,
'current': True,
'discharge': True,
'other': True,
'temperature': True,
'voltage': True,
}),
'sw_version': 'V1.02-V0.00.000',
}),
}),
]),

View File

@@ -1,5 +1,36 @@
# serializer version: 1
# name: test_device
# name: test_device[BAT002]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'zinvolt',
'BAT002',
),
}),
'labels': set({
}),
'manufacturer': 'Zinvolt',
'model': None,
'model_id': 'ZVS4000',
'name': 'Battery - 2',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': 'BAT002',
'sw_version': 'V1.20',
'via_device_id': <ANY>,
})
# ---
# name: test_device[ZVG011025120088]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
@@ -26,7 +57,7 @@
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': 'ZVG011025120088',
'sw_version': 'V1.02-V0.00.000',
'sw_version': 'V1.20',
'via_device_id': None,
})
# ---

View File

@@ -4,7 +4,6 @@ from unittest.mock import AsyncMock
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.zinvolt.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
@@ -22,6 +21,6 @@ async def test_device(
) -> None:
"""Test the Zinvolt device."""
await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device({(DOMAIN, "ZVG011025120088")})
assert device
assert device == snapshot
devices = device_registry.devices
for device in devices.values():
assert device == snapshot(name=list(device.identifiers)[0][1])