This commit is contained in:
Franck Nijhof 2024-10-18 17:06:51 +02:00 committed by GitHub
commit a301d51fb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 446 additions and 71 deletions

View file

@ -85,6 +85,7 @@ HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, HVACMode]] = {
OperationMode.HEATING: HVACMode.HEAT,
OperationMode.FAN: HVACMode.FAN_ONLY,
OperationMode.DRY: HVACMode.DRY,
OperationMode.AUX_HEATING: HVACMode.HEAT,
OperationMode.AUTO: HVACMode.HEAT_COOL,
}
HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = {
@ -157,9 +158,10 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[
self.get_airzone_value(AZD_TEMP_UNIT)
]
self._attr_hvac_modes = [
_attr_hvac_modes = [
HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES)
]
self._attr_hvac_modes = list(dict.fromkeys(_attr_hvac_modes))
if (
self.get_airzone_value(AZD_SPEED) is not None
and self.get_airzone_value(AZD_SPEEDS) is not None

View file

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.3"]
"requirements": ["aioairzone==0.9.5"]
}

View file

@ -6,7 +6,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bluesound",
"iot_class": "local_polling",
"requirements": ["pyblu==1.0.3"],
"requirements": ["pyblu==1.0.4"],
"zeroconf": [
{
"type": "_musc._tcp.local."

View file

@ -493,6 +493,8 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
position = self._status.seconds
if position is None:
return None
if mediastate == MediaPlayerState.PLAYING:
position += (dt_util.utcnow() - self._last_status_update).total_seconds()

View file

@ -159,6 +159,7 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
if values:
await self.device.set(values)
await self.coordinator.async_refresh()
@property
def unique_id(self) -> str:
@ -261,6 +262,7 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
await self.device.set_advanced_mode(
HA_PRESET_TO_DAIKIN[PRESET_ECO], ATTR_STATE_OFF
)
await self.coordinator.async_refresh()
@property
def preset_modes(self) -> list[str]:
@ -275,9 +277,11 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
async def async_turn_on(self) -> None:
"""Turn device on."""
await self.device.set({})
await self.coordinator.async_refresh()
async def async_turn_off(self) -> None:
"""Turn device off."""
await self.device.set(
{HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]}
)
await self.coordinator.async_refresh()

View file

@ -63,10 +63,12 @@ class DaikinZoneSwitch(DaikinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on."""
await self.device.set_zone(self._zone_id, "zone_onoff", "1")
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off."""
await self.device.set_zone(self._zone_id, "zone_onoff", "0")
await self.coordinator.async_refresh()
class DaikinStreamerSwitch(DaikinEntity, SwitchEntity):
@ -88,10 +90,12 @@ class DaikinStreamerSwitch(DaikinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on."""
await self.device.set_streamer("on")
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off."""
await self.device.set_streamer("off")
await self.coordinator.async_refresh()
class DaikinToggleSwitch(DaikinEntity, SwitchEntity):
@ -112,7 +116,9 @@ class DaikinToggleSwitch(DaikinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the zone on."""
await self.device.set({})
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off."""
await self.device.set({DAIKIN_ATTR_MODE: "off"})
await self.coordinator.async_refresh()

View file

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==6.1.5", "oauth2client==4.1.3", "ical==8.2.0"]
"requirements": ["gcal-sync==6.1.6", "oauth2client==4.1.3", "ical==8.2.0"]
}

View file

@ -8,6 +8,6 @@
"iot_class": "local_polling",
"loggers": ["deepmerge", "pyipp"],
"quality_scale": "platinum",
"requirements": ["pyipp==0.16.0"],
"requirements": ["pyipp==0.17.0"],
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
}

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import datetime
from typing import Any
from pyipp import Marker, Printer
@ -19,7 +19,6 @@ from homeassistant.const import ATTR_LOCATION, PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from . import IPPConfigEntry
from .const import (
@ -80,7 +79,7 @@ PRINTER_SENSORS: tuple[IPPSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda printer: (utcnow() - timedelta(seconds=printer.info.uptime)),
value_fn=lambda printer: printer.booted_at,
),
)

View file

@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp==2024.09.27"],
"requirements": ["yt-dlp==2024.10.07"],
"single_config_entry": true
}

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"iot_class": "local_push",
"loggers": ["pyotgw"],
"requirements": ["pyotgw==2.2.1"]
"requirements": ["pyotgw==2.2.2"]
}

View file

@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
from .coordinator import RokuDataUpdateCoordinator
PLATFORMS = [
@ -24,7 +24,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device_id = entry.entry_id
coordinator = RokuDataUpdateCoordinator(
hass, host=entry.data[CONF_HOST], device_id=device_id
hass,
host=entry.data[CONF_HOST],
device_id=device_id,
play_media_app_id=entry.options.get(
CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
),
)
await coordinator.async_config_entry_first_refresh()
@ -32,6 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
return True
@ -40,3 +47,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload the config entry when it changed."""
await hass.config_entries.async_reload(entry.entry_id)

View file

@ -10,12 +10,17 @@ from rokuecp import Roku, RokuError
import voluptuous as vol
from homeassistant.components import ssdp, zeroconf
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
@ -155,3 +160,36 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
title=self.discovery_info[CONF_NAME],
data=self.discovery_info,
)
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlowWithConfigEntry:
"""Create the options flow."""
return RokuOptionsFlowHandler(config_entry)
class RokuOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Roku options."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage Roku options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_PLAY_MEDIA_APP_ID,
default=self.options.get(
CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
),
): str,
}
),
)

View file

@ -15,3 +15,9 @@ DEFAULT_PORT = 8060
# Services
SERVICE_SEARCH = "search"
# Config
CONF_PLAY_MEDIA_APP_ID = "play_media_app_id"
# Defaults
DEFAULT_PLAY_MEDIA_APP_ID = "15985"

View file

@ -29,15 +29,12 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]):
roku: Roku
def __init__(
self,
hass: HomeAssistant,
*,
host: str,
device_id: str,
self, hass: HomeAssistant, *, host: str, device_id: str, play_media_app_id: str
) -> None:
"""Initialize global Roku data updater."""
self.device_id = device_id
self.roku = Roku(host=host, session=async_get_clientsession(hass))
self.play_media_app_id = play_media_app_id
self.full_update_interval = timedelta(minutes=15)
self.last_full_update = None

View file

@ -445,17 +445,25 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
if attr in extra
}
params = {"t": "a", **params}
params = {"u": media_id, "t": "a", **params}
await self.coordinator.roku.play_on_roku(media_id, params)
await self.coordinator.roku.launch(
self.coordinator.play_media_app_id,
params,
)
elif media_type in {MediaType.URL, MediaType.VIDEO}:
params = {
param: extra[attr]
for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items()
if attr in extra
}
params["u"] = media_id
params["t"] = "v"
await self.coordinator.roku.play_on_roku(media_id, params)
await self.coordinator.roku.launch(
self.coordinator.play_media_app_id,
params,
)
else:
_LOGGER.error("Media type %s is not supported", original_media_type)
return

View file

@ -24,6 +24,18 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"options": {
"step": {
"init": {
"data": {
"play_media_app_id": "Play Media Roku Application ID"
},
"data_description": {
"play_media_app_id": "The application ID to use when launching media playback. Must support the PlayOnRoku API."
}
}
}
},
"entity": {
"binary_sensor": {
"headphones_connected": {

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/solarlog",
"iot_class": "local_polling",
"loggers": ["solarlog_cli"],
"requirements": ["solarlog_cli==0.3.1"]
"requirements": ["solarlog_cli==0.3.2"]
}

View file

@ -84,7 +84,9 @@ def _create_entry(
original_name=f"{DEFAULT_NAME} {tag_id}",
suggested_object_id=slugify(name) if name else tag_id,
)
return entity_registry.async_update_entity(entry.entity_id, name=name)
if name:
return entity_registry.async_update_entity(entry.entity_id, name=name)
return entry
class TagStore(Store[collection.SerializedStorageCollection]):

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import ipaddress
import logging
from typing import Any
@ -38,7 +39,19 @@ class WebControlProConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the DHCP discovery step."""
unique_id = format_mac(discovery_info.macaddress)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
entry = self.hass.config_entries.async_entry_for_domain_unique_id(
DOMAIN, unique_id
)
if entry:
try: # Check if current host is a valid IP address
ipaddress.ip_address(entry.data[CONF_HOST])
except ValueError: # Do not touch name-based host
return self.async_abort(reason="already_configured")
else: # Update existing host with new IP address
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info.ip}
)
for entry in self.hass.config_entries.async_entries(DOMAIN):
if not entry.unique_id and entry.data[CONF_HOST] in (

View file

@ -1198,7 +1198,7 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData:
# deep copy the yaml config to avoid modifying the original and to safely
# pass it to the ZHA library
app_config = copy.deepcopy(ha_zha_data.yaml_config.get(CONF_ZIGPY, {}))
database = app_config.get(
database = ha_zha_data.yaml_config.get(
CONF_DATABASE,
hass.config.path(DEFAULT_DATABASE_NAME),
)

View file

@ -24,8 +24,6 @@ from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DEFAULT_MAX_TEMP,
DEFAULT_MIN_TEMP,
DOMAIN as CLIMATE_DOMAIN,
PRESET_NONE,
ClimateEntity,
@ -421,7 +419,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@property
def min_temp(self) -> float:
"""Return the minimum temperature."""
min_temp = DEFAULT_MIN_TEMP
min_temp = 0.0 # Not using DEFAULT_MIN_TEMP to allow wider range
base_unit: str = UnitOfTemperature.CELSIUS
try:
temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0])
@ -437,7 +435,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@property
def max_temp(self) -> float:
"""Return the maximum temperature."""
max_temp = DEFAULT_MAX_TEMP
max_temp = 50.0 # Not using DEFAULT_MAX_TEMP to allow wider range
base_unit: str = UnitOfTemperature.CELSIUS
try:
temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0])

View file

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

View file

@ -31,7 +31,7 @@ ha-ffmpeg==3.2.0
habluetooth==3.4.0
hass-nabucasa==0.81.1
hassil==1.7.4
home-assistant-bluetooth==1.12.2
home-assistant-bluetooth==1.13.0
home-assistant-frontend==20241002.3
home-assistant-intents==2024.10.2
httpx==0.27.2

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.10.2"
version = "2024.10.3"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@ -46,7 +46,7 @@ dependencies = [
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.27.2",
"home-assistant-bluetooth==1.12.2",
"home-assistant-bluetooth==1.13.0",
"ifaddr==0.2.0",
"Jinja2==3.1.4",
"lru-dict==1.3.0",

View file

@ -20,7 +20,7 @@ ciso8601==2.3.1
fnv-hash-fast==1.0.2
hass-nabucasa==0.81.1
httpx==0.27.2
home-assistant-bluetooth==1.12.2
home-assistant-bluetooth==1.13.0
ifaddr==0.2.0
Jinja2==3.1.4
lru-dict==1.3.0

View file

@ -179,7 +179,7 @@ aioairq==0.3.2
aioairzone-cloud==0.6.6
# homeassistant.components.airzone
aioairzone==0.9.3
aioairzone==0.9.5
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@ -945,7 +945,7 @@ gardena-bluetooth==1.4.3
gassist-text==0.0.11
# homeassistant.components.google
gcal-sync==6.1.5
gcal-sync==6.1.6
# homeassistant.components.geniushub
geniushub-client==0.7.1
@ -1780,7 +1780,7 @@ pybbox==0.0.5-alpha
pyblackbird==0.6
# homeassistant.components.bluesound
pyblu==1.0.3
pyblu==1.0.4
# homeassistant.components.neato
pybotvac==0.0.25
@ -1960,7 +1960,7 @@ pyintesishome==1.8.0
pyipma==3.0.7
# homeassistant.components.ipp
pyipp==0.16.0
pyipp==0.17.0
# homeassistant.components.iqvia
pyiqvia==2022.04.0
@ -2119,7 +2119,7 @@ pyoppleio-legacy==1.0.8
pyosoenergyapi==1.1.4
# homeassistant.components.opentherm_gw
pyotgw==2.2.1
pyotgw==2.2.2
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@ -2676,7 +2676,7 @@ soco==0.30.4
solaredge-local==0.2.3
# homeassistant.components.solarlog
solarlog_cli==0.3.1
solarlog_cli==0.3.2
# homeassistant.components.solax
solax==3.1.1
@ -3032,7 +3032,7 @@ youless-api==2.1.2
youtubeaio==1.1.5
# homeassistant.components.media_extractor
yt-dlp==2024.09.27
yt-dlp==2024.10.07
# homeassistant.components.zamg
zamg==0.3.6

View file

@ -167,7 +167,7 @@ aioairq==0.3.2
aioairzone-cloud==0.6.6
# homeassistant.components.airzone
aioairzone==0.9.3
aioairzone==0.9.5
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@ -798,7 +798,7 @@ gardena-bluetooth==1.4.3
gassist-text==0.0.11
# homeassistant.components.google
gcal-sync==6.1.5
gcal-sync==6.1.6
# homeassistant.components.geniushub
geniushub-client==0.7.1
@ -1448,7 +1448,7 @@ pybalboa==1.0.2
pyblackbird==0.6
# homeassistant.components.bluesound
pyblu==1.0.3
pyblu==1.0.4
# homeassistant.components.neato
pybotvac==0.0.25
@ -1574,7 +1574,7 @@ pyinsteon==1.6.3
pyipma==3.0.7
# homeassistant.components.ipp
pyipp==0.16.0
pyipp==0.17.0
# homeassistant.components.iqvia
pyiqvia==2022.04.0
@ -1703,7 +1703,7 @@ pyopnsense==0.4.0
pyosoenergyapi==1.1.4
# homeassistant.components.opentherm_gw
pyotgw==2.2.1
pyotgw==2.2.2
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@ -2122,7 +2122,7 @@ snapcast==2.3.6
soco==0.30.4
# homeassistant.components.solarlog
solarlog_cli==0.3.1
solarlog_cli==0.3.2
# homeassistant.components.solax
solax==3.1.1
@ -2415,7 +2415,7 @@ youless-api==2.1.2
youtubeaio==1.1.5
# homeassistant.components.media_extractor
yt-dlp==2024.09.27
yt-dlp==2024.10.07
# homeassistant.components.zamg
zamg==0.3.6

View file

@ -220,6 +220,45 @@
}),
]),
}),
dict({
'data': list([
dict({
'air_demand': 0,
'coldStage': 0,
'coldStages': 0,
'coolmaxtemp': 30,
'coolmintemp': 15,
'coolsetpoint': 20,
'errors': list([
]),
'floor_demand': 0,
'heatStage': 0,
'heatStages': 0,
'heatmaxtemp': 30,
'heatmintemp': 15,
'heatsetpoint': 20,
'humidity': 0,
'maxTemp': 30,
'minTemp': 15,
'mode': 6,
'modes': list([
1,
2,
3,
4,
5,
6,
]),
'name': 'Aux Heat',
'on': 1,
'roomTemp': 22,
'setpoint': 20,
'systemID': 4,
'units': 0,
'zoneID': 1,
}),
]),
}),
]),
}),
'version': dict({
@ -269,8 +308,8 @@
'temp-set': 45,
'temp-unit': 0,
}),
'num-systems': 3,
'num-zones': 7,
'num-systems': 4,
'num-zones': 8,
'systems': dict({
'1': dict({
'available': True,
@ -320,6 +359,23 @@
]),
'problems': False,
}),
'4': dict({
'available': True,
'full-name': 'Airzone [4] System',
'id': 4,
'master-system-zone': '4:1',
'master-zone': 1,
'mode': 6,
'modes': list([
1,
2,
3,
4,
5,
6,
]),
'problems': False,
}),
}),
'version': '1.62',
'webserver': dict({
@ -683,6 +739,46 @@
'temp-step': 1.0,
'temp-unit': 1,
}),
'4:1': dict({
'absolute-temp-max': 30.0,
'absolute-temp-min': 15.0,
'action': 5,
'air-demand': False,
'available': True,
'cold-stage': 0,
'cool-temp-max': 30.0,
'cool-temp-min': 15.0,
'cool-temp-set': 20.0,
'demand': False,
'double-set-point': False,
'floor-demand': False,
'full-name': 'Airzone [4:1] Aux Heat',
'heat-stage': 0,
'heat-temp-max': 30.0,
'heat-temp-min': 15.0,
'heat-temp-set': 20.0,
'id': 1,
'master': True,
'mode': 6,
'modes': list([
1,
2,
3,
4,
5,
6,
]),
'name': 'Aux Heat',
'on': True,
'problems': False,
'system': 4,
'temp': 22.0,
'temp-max': 30.0,
'temp-min': 15.0,
'temp-set': 20.0,
'temp-step': 0.5,
'temp-unit': 0,
}),
}),
}),
})

View file

@ -225,6 +225,23 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None:
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 22.8
state = hass.states.get("climate.aux_heat")
assert state.state == HVACMode.HEAT
assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None
assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 22
assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.IDLE
assert state.attributes.get(ATTR_HVAC_MODES) == [
HVACMode.OFF,
HVACMode.COOL,
HVACMode.HEAT,
HVACMode.FAN_ONLY,
HVACMode.DRY,
]
assert state.attributes.get(ATTR_MAX_TEMP) == 30
assert state.attributes.get(ATTR_MIN_TEMP) == 15
assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP
assert state.attributes.get(ATTR_TEMPERATURE) == 20.0
HVAC_MOCK_CHANGED = copy.deepcopy(HVAC_MOCK)
HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25
HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MIN_TEMP] = 10

View file

@ -272,6 +272,37 @@ HVAC_MOCK = {
},
]
},
{
API_DATA: [
{
API_SYSTEM_ID: 4,
API_ZONE_ID: 1,
API_NAME: "Aux Heat",
API_ON: 1,
API_COOL_SET_POINT: 20,
API_COOL_MAX_TEMP: 30,
API_COOL_MIN_TEMP: 15,
API_HEAT_SET_POINT: 20,
API_HEAT_MAX_TEMP: 30,
API_HEAT_MIN_TEMP: 15,
API_MAX_TEMP: 30,
API_MIN_TEMP: 15,
API_SET_POINT: 20,
API_ROOM_TEMP: 22,
API_MODES: [1, 2, 3, 4, 5, 6],
API_MODE: 6,
API_COLD_STAGES: 0,
API_COLD_STAGE: 0,
API_HEAT_STAGES: 0,
API_HEAT_STAGE: 0,
API_HUMIDITY: 0,
API_UNITS: 0,
API_ERRORS: [],
API_AIR_DEMAND: 0,
API_FLOOR_DEMAND: 0,
},
]
},
]
}

View file

@ -2,6 +2,7 @@
# name: test_diagnostics
dict({
'data': dict({
'booted_at': '2019-11-11T09:10:02+00:00',
'info': dict({
'command_set': 'ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF',
'location': None,

View file

@ -1,5 +1,6 @@
"""Tests for the diagnostics data provided by the Internet Printing Protocol (IPP) integration."""
import pytest
from syrupy import SnapshotAssertion
from homeassistant.core import HomeAssistant
@ -9,6 +10,7 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
@pytest.mark.freeze_time("2019-11-11 09:10:32+00:00")
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,

View file

@ -6,7 +6,7 @@ from unittest.mock import MagicMock
import pytest
from rokuecp import RokuConnectionError
from homeassistant.components.roku.const import DOMAIN
from homeassistant.components.roku.const import CONF_PLAY_MEDIA_APP_ID, DOMAIN
from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant
@ -254,3 +254,25 @@ async def test_ssdp_discovery(
assert result["data"]
assert result["data"][CONF_HOST] == HOST
assert result["data"][CONF_NAME] == UPNP_FRIENDLY_NAME
async def test_options_flow(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test options config flow."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "init"
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_PLAY_MEDIA_APP_ID: "782875"},
)
assert result2.get("type") is FlowResultType.CREATE_ENTRY
assert result2.get("data") == {
CONF_PLAY_MEDIA_APP_ID: "782875",
}

View file

@ -32,6 +32,7 @@ from homeassistant.components.roku.const import (
ATTR_FORMAT,
ATTR_KEYWORD,
ATTR_MEDIA_TYPE,
DEFAULT_PLAY_MEDIA_APP_ID,
DOMAIN,
SERVICE_SEARCH,
)
@ -495,7 +496,7 @@ async def test_services_play_media(
blocking=True,
)
assert mock_roku.play_on_roku.call_count == 0
assert mock_roku.launch.call_count == 0
await hass.services.async_call(
MP_DOMAIN,
@ -509,7 +510,7 @@ async def test_services_play_media(
blocking=True,
)
assert mock_roku.play_on_roku.call_count == 0
assert mock_roku.launch.call_count == 0
@pytest.mark.parametrize(
@ -546,9 +547,10 @@ async def test_services_play_media_audio(
},
blocking=True,
)
mock_roku.play_on_roku.assert_called_once_with(
content_id,
mock_roku.launch.assert_called_once_with(
DEFAULT_PLAY_MEDIA_APP_ID,
{
"u": content_id,
"t": "a",
"songName": resolved_name,
"songFormat": resolved_format,
@ -591,9 +593,11 @@ async def test_services_play_media_video(
},
blocking=True,
)
mock_roku.play_on_roku.assert_called_once_with(
content_id,
mock_roku.launch.assert_called_once_with(
DEFAULT_PLAY_MEDIA_APP_ID,
{
"u": content_id,
"t": "v",
"videoName": resolved_name,
"videoFormat": resolved_format,
},
@ -617,10 +621,12 @@ async def test_services_camera_play_stream(
blocking=True,
)
assert mock_roku.play_on_roku.call_count == 1
mock_roku.play_on_roku.assert_called_with(
"https://awesome.tld/api/hls/api_token/master_playlist.m3u8",
assert mock_roku.launch.call_count == 1
mock_roku.launch.assert_called_with(
DEFAULT_PLAY_MEDIA_APP_ID,
{
"u": "https://awesome.tld/api/hls/api_token/master_playlist.m3u8",
"t": "v",
"videoName": "Camera Stream",
"videoFormat": "hls",
},
@ -653,14 +659,21 @@ async def test_services_play_media_local_source(
blocking=True,
)
assert mock_roku.play_on_roku.call_count == 1
assert mock_roku.play_on_roku.call_args
call_args = mock_roku.play_on_roku.call_args.args
assert "/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0]
assert call_args[1] == {
"videoFormat": "mp4",
"videoName": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4",
}
assert mock_roku.launch.call_count == 1
assert mock_roku.launch.call_args
call_args = mock_roku.launch.call_args.args
assert call_args[0] == DEFAULT_PLAY_MEDIA_APP_ID
assert "u" in call_args[1]
assert "/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[1]["u"]
assert "t" in call_args[1]
assert call_args[1]["t"] == "v"
assert "videoFormat" in call_args[1]
assert call_args[1]["videoFormat"] == "mp4"
assert "videoName" in call_args[1]
assert (
call_args[1]["videoName"]
== "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4"
)
@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True)

View file

@ -294,6 +294,10 @@ async def test_entity_created_and_removed(
assert item["id"] == "1234567890"
assert item["name"] == "Kitchen tag"
await hass.async_block_till_done()
er_entity = entity_registry.async_get("tag.kitchen_tag")
assert er_entity.name == "Kitchen tag"
entity = hass.states.get("tag.kitchen_tag")
assert entity
assert entity.state == STATE_UNKNOWN

View file

@ -112,6 +112,96 @@ async def test_config_flow_from_dhcp_add_mac(
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
async def test_config_flow_from_dhcp_ip_update(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
) -> None:
"""Test we can use DHCP discovery to update IP in a config entry."""
info = DhcpServiceInfo(
ip="1.2.3.4", hostname="webcontrol", macaddress="00:11:22:33:44:55"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=info
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
with patch(
"wmspro.webcontrol.WebControlPro.ping",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.2.3.4",
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "1.2.3.4"
assert result["data"] == {
CONF_HOST: "1.2.3.4",
}
assert len(mock_setup_entry.mock_calls) == 1
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
info = DhcpServiceInfo(
ip="5.6.7.8", hostname="webcontrol", macaddress="00:11:22:33:44:55"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=info
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
assert hass.config_entries.async_entries(DOMAIN)[0].data[CONF_HOST] == "5.6.7.8"
async def test_config_flow_from_dhcp_no_update(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
) -> None:
"""Test we do not use DHCP discovery to overwrite hostname with IP in config entry."""
info = DhcpServiceInfo(
ip="1.2.3.4", hostname="webcontrol", macaddress="00:11:22:33:44:55"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=info
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
with patch(
"wmspro.webcontrol.WebControlPro.ping",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "webcontrol",
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "webcontrol"
assert result["data"] == {
CONF_HOST: "webcontrol",
}
assert len(mock_setup_entry.mock_calls) == 1
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
info = DhcpServiceInfo(
ip="5.6.7.8", hostname="webcontrol", macaddress="00:11:22:33:44:55"
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=info
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "00:11:22:33:44:55"
assert hass.config_entries.async_entries(DOMAIN)[0].data[CONF_HOST] == "webcontrol"
async def test_config_flow_ping_failed(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:

View file

@ -812,8 +812,8 @@ async def test_thermostat_heatit_z_trm2fx(
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
assert state.attributes[ATTR_MIN_TEMP] == 7
assert state.attributes[ATTR_MAX_TEMP] == 35
assert state.attributes[ATTR_MIN_TEMP] == 0
assert state.attributes[ATTR_MAX_TEMP] == 50
# Try switching to external sensor
event = Event(