mirror of
https://github.com/home-assistant/core.git
synced 2025-07-30 00:27:19 +00:00
commit
becc120ad9
10
build.json
10
build.json
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"image": "homeassistant/{arch}-homeassistant",
|
"image": "homeassistant/{arch}-homeassistant",
|
||||||
"build_from": {
|
"build_from": {
|
||||||
"aarch64": "homeassistant/aarch64-homeassistant-base:7.1.0",
|
"aarch64": "homeassistant/aarch64-homeassistant-base:7.2.0",
|
||||||
"armhf": "homeassistant/armhf-homeassistant-base:7.1.0",
|
"armhf": "homeassistant/armhf-homeassistant-base:7.2.0",
|
||||||
"armv7": "homeassistant/armv7-homeassistant-base:7.1.0",
|
"armv7": "homeassistant/armv7-homeassistant-base:7.2.0",
|
||||||
"amd64": "homeassistant/amd64-homeassistant-base:7.1.0",
|
"amd64": "homeassistant/amd64-homeassistant-base:7.2.0",
|
||||||
"i386": "homeassistant/i386-homeassistant-base:7.1.0"
|
"i386": "homeassistant/i386-homeassistant-base:7.2.0"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"io.hass.type": "core"
|
"io.hass.type": "core"
|
||||||
|
@ -5,7 +5,7 @@ from bravia_tv import BraviaRC
|
|||||||
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_MAC
|
from homeassistant.const import CONF_HOST, CONF_MAC
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import BRAVIARC, DOMAIN, UNDO_UPDATE_LISTENER
|
||||||
|
|
||||||
PLATFORMS = ["media_player"]
|
PLATFORMS = ["media_player"]
|
||||||
|
|
||||||
@ -20,8 +20,13 @@ async def async_setup_entry(hass, config_entry):
|
|||||||
host = config_entry.data[CONF_HOST]
|
host = config_entry.data[CONF_HOST]
|
||||||
mac = config_entry.data[CONF_MAC]
|
mac = config_entry.data[CONF_MAC]
|
||||||
|
|
||||||
|
undo_listener = config_entry.add_update_listener(update_listener)
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
hass.data[DOMAIN][config_entry.entry_id] = BraviaRC(host, mac)
|
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||||
|
BRAVIARC: BraviaRC(host, mac),
|
||||||
|
UNDO_UPDATE_LISTENER: undo_listener,
|
||||||
|
}
|
||||||
|
|
||||||
for component in PLATFORMS:
|
for component in PLATFORMS:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
@ -41,7 +46,15 @@ async def async_unload_entry(hass, config_entry):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||||
|
|
||||||
if unload_ok:
|
if unload_ok:
|
||||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def update_listener(hass, config_entry):
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
@ -15,6 +15,7 @@ from .const import ( # pylint:disable=unused-import
|
|||||||
ATTR_CID,
|
ATTR_CID,
|
||||||
ATTR_MAC,
|
ATTR_MAC,
|
||||||
ATTR_MODEL,
|
ATTR_MODEL,
|
||||||
|
BRAVIARC,
|
||||||
CLIENTID_PREFIX,
|
CLIENTID_PREFIX,
|
||||||
CONF_IGNORED_SOURCES,
|
CONF_IGNORED_SOURCES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -152,7 +153,7 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
|
|
||||||
async def async_step_init(self, user_input=None):
|
async def async_step_init(self, user_input=None):
|
||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
self.braviarc = self.hass.data[DOMAIN][self.config_entry.entry_id]
|
self.braviarc = self.hass.data[DOMAIN][self.config_entry.entry_id][BRAVIARC]
|
||||||
if not self.braviarc.is_connected():
|
if not self.braviarc.is_connected():
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
self.braviarc.connect, self.pin, CLIENTID_PREFIX, NICKNAME,
|
self.braviarc.connect, self.pin, CLIENTID_PREFIX, NICKNAME,
|
||||||
|
@ -6,8 +6,10 @@ ATTR_MODEL = "model"
|
|||||||
|
|
||||||
CONF_IGNORED_SOURCES = "ignored_sources"
|
CONF_IGNORED_SOURCES = "ignored_sources"
|
||||||
|
|
||||||
|
BRAVIARC = "braviarc"
|
||||||
BRAVIA_CONFIG_FILE = "bravia.conf"
|
BRAVIA_CONFIG_FILE = "bravia.conf"
|
||||||
CLIENTID_PREFIX = "HomeAssistant"
|
CLIENTID_PREFIX = "HomeAssistant"
|
||||||
DEFAULT_NAME = f"{ATTR_MANUFACTURER} Bravia TV"
|
DEFAULT_NAME = f"{ATTR_MANUFACTURER} Bravia TV"
|
||||||
DOMAIN = "braviatv"
|
DOMAIN = "braviatv"
|
||||||
NICKNAME = "Home Assistant"
|
NICKNAME = "Home Assistant"
|
||||||
|
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||||
|
@ -30,6 +30,7 @@ from homeassistant.util.json import load_json
|
|||||||
from .const import (
|
from .const import (
|
||||||
ATTR_MANUFACTURER,
|
ATTR_MANUFACTURER,
|
||||||
BRAVIA_CONFIG_FILE,
|
BRAVIA_CONFIG_FILE,
|
||||||
|
BRAVIARC,
|
||||||
CLIENTID_PREFIX,
|
CLIENTID_PREFIX,
|
||||||
CONF_IGNORED_SOURCES,
|
CONF_IGNORED_SOURCES,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
@ -103,7 +104,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
"model": config_entry.title,
|
"model": config_entry.title,
|
||||||
}
|
}
|
||||||
|
|
||||||
braviarc = hass.data[DOMAIN][config_entry.entry_id]
|
braviarc = hass.data[DOMAIN][config_entry.entry_id][BRAVIARC]
|
||||||
|
|
||||||
ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, [])
|
ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, [])
|
||||||
|
|
||||||
|
@ -131,15 +131,6 @@ class FluNearYouData:
|
|||||||
self.latitude = latitude
|
self.latitude = latitude
|
||||||
self.longitude = longitude
|
self.longitude = longitude
|
||||||
|
|
||||||
self._api_coros = {
|
|
||||||
CATEGORY_CDC_REPORT: self._client.cdc_reports.status_by_coordinates(
|
|
||||||
latitude, longitude
|
|
||||||
),
|
|
||||||
CATEGORY_USER_REPORT: self._client.user_reports.status_by_coordinates(
|
|
||||||
latitude, longitude
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
self._api_category_count = {
|
self._api_category_count = {
|
||||||
CATEGORY_CDC_REPORT: 0,
|
CATEGORY_CDC_REPORT: 0,
|
||||||
CATEGORY_USER_REPORT: 0,
|
CATEGORY_USER_REPORT: 0,
|
||||||
@ -155,8 +146,17 @@ class FluNearYouData:
|
|||||||
if self._api_category_count[api_category] == 0:
|
if self._api_category_count[api_category] == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if api_category == CATEGORY_CDC_REPORT:
|
||||||
|
api_coro = self._client.cdc_reports.status_by_coordinates(
|
||||||
|
self.latitude, self.longitude
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
api_coro = self._client.user_reports.status_by_coordinates(
|
||||||
|
self.latitude, self.longitude
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.data[api_category] = await self._api_coros[api_category]
|
self.data[api_category] = await api_coro
|
||||||
except FluNearYouError as err:
|
except FluNearYouError as err:
|
||||||
LOGGER.error("Unable to get %s data: %s", api_category, err)
|
LOGGER.error("Unable to get %s data: %s", api_category, err)
|
||||||
self.data[api_category] = None
|
self.data[api_category] = None
|
||||||
@ -200,7 +200,7 @@ class FluNearYouData:
|
|||||||
"""Update Flu Near You data."""
|
"""Update Flu Near You data."""
|
||||||
tasks = [
|
tasks = [
|
||||||
self._async_get_data_from_api(api_category)
|
self._async_get_data_from_api(api_category)
|
||||||
for api_category in self._api_coros
|
for api_category in self._api_category_count
|
||||||
]
|
]
|
||||||
|
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
@ -86,6 +86,7 @@ class GarminConnectData:
|
|||||||
|
|
||||||
def __init__(self, hass, client):
|
def __init__(self, hass, client):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
|
self.hass = hass
|
||||||
self.client = client
|
self.client = client
|
||||||
self.data = None
|
self.data = None
|
||||||
|
|
||||||
@ -95,7 +96,9 @@ class GarminConnectData:
|
|||||||
today = date.today()
|
today = date.today()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.data = self.client.get_stats_and_body(today.isoformat())
|
self.data = await self.hass.async_add_executor_job(
|
||||||
|
self.client.get_stats_and_body, today.isoformat()
|
||||||
|
)
|
||||||
except (
|
except (
|
||||||
GarminConnectAuthenticationError,
|
GarminConnectAuthenticationError,
|
||||||
GarminConnectTooManyRequestsError,
|
GarminConnectTooManyRequestsError,
|
||||||
|
@ -99,6 +99,9 @@ class HomeAccessory(Accessory):
|
|||||||
self.entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.debounce = {}
|
self.debounce = {}
|
||||||
|
self._char_battery = None
|
||||||
|
self._char_charging = None
|
||||||
|
self._char_low_battery = None
|
||||||
self.linked_battery_sensor = self.config.get(CONF_LINKED_BATTERY_SENSOR)
|
self.linked_battery_sensor = self.config.get(CONF_LINKED_BATTERY_SENSOR)
|
||||||
self.linked_battery_charging_sensor = self.config.get(
|
self.linked_battery_charging_sensor = self.config.get(
|
||||||
CONF_LINKED_BATTERY_CHARGING_SENSOR
|
CONF_LINKED_BATTERY_CHARGING_SENSOR
|
||||||
@ -247,6 +250,10 @@ class HomeAccessory(Accessory):
|
|||||||
|
|
||||||
Only call this function if self._support_battery_level is True.
|
Only call this function if self._support_battery_level is True.
|
||||||
"""
|
"""
|
||||||
|
if not self._char_battery:
|
||||||
|
# Battery appeared after homekit was started
|
||||||
|
return
|
||||||
|
|
||||||
battery_level = convert_to_float(battery_level)
|
battery_level = convert_to_float(battery_level)
|
||||||
if battery_level is not None:
|
if battery_level is not None:
|
||||||
if self._char_battery.value != battery_level:
|
if self._char_battery.value != battery_level:
|
||||||
@ -258,7 +265,8 @@ class HomeAccessory(Accessory):
|
|||||||
"%s: Updated battery level to %d", self.entity_id, battery_level
|
"%s: Updated battery level to %d", self.entity_id, battery_level
|
||||||
)
|
)
|
||||||
|
|
||||||
if battery_charging is None:
|
# Charging state can appear after homekit was started
|
||||||
|
if battery_charging is None or not self._char_charging:
|
||||||
return
|
return
|
||||||
|
|
||||||
hk_charging = HK_CHARGING if battery_charging else HK_NOT_CHARGING
|
hk_charging = HK_CHARGING if battery_charging else HK_NOT_CHARGING
|
||||||
|
@ -94,13 +94,13 @@ class Fan(HomeAccessory):
|
|||||||
_LOGGER.debug("Fan _set_chars: %s", char_values)
|
_LOGGER.debug("Fan _set_chars: %s", char_values)
|
||||||
if CHAR_ACTIVE in char_values:
|
if CHAR_ACTIVE in char_values:
|
||||||
if char_values[CHAR_ACTIVE]:
|
if char_values[CHAR_ACTIVE]:
|
||||||
is_on = False
|
# If the device supports set speed we
|
||||||
state = self.hass.states.get(self.entity_id)
|
# do not want to turn on as it will take
|
||||||
if state and state.state == STATE_ON:
|
# the fan to 100% than to the desired speed.
|
||||||
is_on = True
|
#
|
||||||
# Only set the state to active if we
|
# Setting the speed will take care of turning
|
||||||
# did not get a rotation speed or its off
|
# on the fan if SUPPORT_SET_SPEED is set.
|
||||||
if not is_on or CHAR_ROTATION_SPEED not in char_values:
|
if not self.char_speed or CHAR_ROTATION_SPEED not in char_values:
|
||||||
self.set_state(1)
|
self.set_state(1)
|
||||||
else:
|
else:
|
||||||
# Its off, nothing more to do as setting the
|
# Its off, nothing more to do as setting the
|
||||||
|
@ -117,12 +117,15 @@ class Thermostat(HomeAccessory):
|
|||||||
"""Initialize a Thermostat accessory object."""
|
"""Initialize a Thermostat accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_THERMOSTAT)
|
super().__init__(*args, category=CATEGORY_THERMOSTAT)
|
||||||
self._unit = self.hass.config.units.temperature_unit
|
self._unit = self.hass.config.units.temperature_unit
|
||||||
|
self._state_updates = 0
|
||||||
|
self.hc_homekit_to_hass = None
|
||||||
|
self.hc_hass_to_homekit = None
|
||||||
min_temp, max_temp = self.get_temperature_range()
|
min_temp, max_temp = self.get_temperature_range()
|
||||||
|
|
||||||
# Homekit only supports 10-38, overwriting
|
# Homekit only supports 10-38, overwriting
|
||||||
# the max to appears to work, but less than 10 causes
|
# the max to appears to work, but less than 0 causes
|
||||||
# a crash on the home app
|
# a crash on the home app
|
||||||
hc_min_temp = max(min_temp, HC_MIN_TEMP)
|
hc_min_temp = max(min_temp, 0)
|
||||||
hc_max_temp = max_temp
|
hc_max_temp = max_temp
|
||||||
|
|
||||||
min_humidity = self.hass.states.get(self.entity_id).attributes.get(
|
min_humidity = self.hass.states.get(self.entity_id).attributes.get(
|
||||||
@ -149,48 +152,17 @@ class Thermostat(HomeAccessory):
|
|||||||
CHAR_CURRENT_HEATING_COOLING, value=0
|
CHAR_CURRENT_HEATING_COOLING, value=0
|
||||||
)
|
)
|
||||||
|
|
||||||
# Target mode characteristics
|
self._configure_hvac_modes(state)
|
||||||
hc_modes = state.attributes.get(ATTR_HVAC_MODES)
|
# Must set the value first as setting
|
||||||
if hc_modes is None:
|
# valid_values happens before setting
|
||||||
_LOGGER.error(
|
# the value and if 0 is not a valid
|
||||||
"%s: HVAC modes not yet available. Please disable auto start for homekit.",
|
# value this will throw
|
||||||
self.entity_id,
|
|
||||||
)
|
|
||||||
hc_modes = (
|
|
||||||
HVAC_MODE_HEAT,
|
|
||||||
HVAC_MODE_COOL,
|
|
||||||
HVAC_MODE_HEAT_COOL,
|
|
||||||
HVAC_MODE_OFF,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine available modes for this entity,
|
|
||||||
# Prefer HEAT_COOL over AUTO and COOL over FAN_ONLY, DRY
|
|
||||||
#
|
|
||||||
# HEAT_COOL is preferred over auto because HomeKit Accessory Protocol describes
|
|
||||||
# heating or cooling comes on to maintain a target temp which is closest to
|
|
||||||
# the Home Assistant spec
|
|
||||||
#
|
|
||||||
# HVAC_MODE_HEAT_COOL: The device supports heating/cooling to a range
|
|
||||||
self.hc_homekit_to_hass = {
|
|
||||||
c: s
|
|
||||||
for s, c in HC_HASS_TO_HOMEKIT.items()
|
|
||||||
if (
|
|
||||||
s in hc_modes
|
|
||||||
and not (
|
|
||||||
(s == HVAC_MODE_AUTO and HVAC_MODE_HEAT_COOL in hc_modes)
|
|
||||||
or (
|
|
||||||
s in (HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY)
|
|
||||||
and HVAC_MODE_COOL in hc_modes
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
hc_valid_values = {k: v for v, k in self.hc_homekit_to_hass.items()}
|
|
||||||
|
|
||||||
self.char_target_heat_cool = serv_thermostat.configure_char(
|
self.char_target_heat_cool = serv_thermostat.configure_char(
|
||||||
CHAR_TARGET_HEATING_COOLING, valid_values=hc_valid_values,
|
CHAR_TARGET_HEATING_COOLING, value=list(self.hc_homekit_to_hass)[0]
|
||||||
|
)
|
||||||
|
self.char_target_heat_cool.override_properties(
|
||||||
|
valid_values=self.hc_hass_to_homekit
|
||||||
)
|
)
|
||||||
|
|
||||||
# Current and target temperature characteristics
|
# Current and target temperature characteristics
|
||||||
|
|
||||||
self.char_current_temp = serv_thermostat.configure_char(
|
self.char_current_temp = serv_thermostat.configure_char(
|
||||||
@ -249,7 +221,7 @@ class Thermostat(HomeAccessory):
|
|||||||
CHAR_CURRENT_HUMIDITY, value=50
|
CHAR_CURRENT_HUMIDITY, value=50
|
||||||
)
|
)
|
||||||
|
|
||||||
self.update_state(state)
|
self._update_state(state)
|
||||||
|
|
||||||
serv_thermostat.setter_callback = self._set_chars
|
serv_thermostat.setter_callback = self._set_chars
|
||||||
|
|
||||||
@ -356,6 +328,46 @@ class Thermostat(HomeAccessory):
|
|||||||
if CHAR_TARGET_HUMIDITY in char_values:
|
if CHAR_TARGET_HUMIDITY in char_values:
|
||||||
self.set_target_humidity(char_values[CHAR_TARGET_HUMIDITY])
|
self.set_target_humidity(char_values[CHAR_TARGET_HUMIDITY])
|
||||||
|
|
||||||
|
def _configure_hvac_modes(self, state):
|
||||||
|
"""Configure target mode characteristics."""
|
||||||
|
hc_modes = state.attributes.get(ATTR_HVAC_MODES)
|
||||||
|
if not hc_modes:
|
||||||
|
# This cannot be none OR an empty list
|
||||||
|
_LOGGER.error(
|
||||||
|
"%s: HVAC modes not yet available. Please disable auto start for homekit.",
|
||||||
|
self.entity_id,
|
||||||
|
)
|
||||||
|
hc_modes = (
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine available modes for this entity,
|
||||||
|
# Prefer HEAT_COOL over AUTO and COOL over FAN_ONLY, DRY
|
||||||
|
#
|
||||||
|
# HEAT_COOL is preferred over auto because HomeKit Accessory Protocol describes
|
||||||
|
# heating or cooling comes on to maintain a target temp which is closest to
|
||||||
|
# the Home Assistant spec
|
||||||
|
#
|
||||||
|
# HVAC_MODE_HEAT_COOL: The device supports heating/cooling to a range
|
||||||
|
self.hc_homekit_to_hass = {
|
||||||
|
c: s
|
||||||
|
for s, c in HC_HASS_TO_HOMEKIT.items()
|
||||||
|
if (
|
||||||
|
s in hc_modes
|
||||||
|
and not (
|
||||||
|
(s == HVAC_MODE_AUTO and HVAC_MODE_HEAT_COOL in hc_modes)
|
||||||
|
or (
|
||||||
|
s in (HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY)
|
||||||
|
and HVAC_MODE_COOL in hc_modes
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
self.hc_hass_to_homekit = {k: v for v, k in self.hc_homekit_to_hass.items()}
|
||||||
|
|
||||||
def get_temperature_range(self):
|
def get_temperature_range(self):
|
||||||
"""Return min and max temperature range."""
|
"""Return min and max temperature range."""
|
||||||
max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP)
|
max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP)
|
||||||
@ -382,14 +394,46 @@ class Thermostat(HomeAccessory):
|
|||||||
|
|
||||||
def update_state(self, new_state):
|
def update_state(self, new_state):
|
||||||
"""Update thermostat state after state changed."""
|
"""Update thermostat state after state changed."""
|
||||||
|
if self._state_updates < 3:
|
||||||
|
# When we get the first state updates
|
||||||
|
# we recheck valid hvac modes as the entity
|
||||||
|
# may not have been fully setup when we saw it the
|
||||||
|
# first time
|
||||||
|
original_hc_hass_to_homekit = self.hc_hass_to_homekit
|
||||||
|
self._configure_hvac_modes(new_state)
|
||||||
|
if self.hc_hass_to_homekit != original_hc_hass_to_homekit:
|
||||||
|
if self.char_target_heat_cool.value not in self.hc_homekit_to_hass:
|
||||||
|
# We must make sure the char value is
|
||||||
|
# in the new valid values before
|
||||||
|
# setting the new valid values or
|
||||||
|
# changing them with throw
|
||||||
|
self.char_target_heat_cool.set_value(
|
||||||
|
list(self.hc_homekit_to_hass)[0], should_notify=False
|
||||||
|
)
|
||||||
|
self.char_target_heat_cool.override_properties(
|
||||||
|
valid_values=self.hc_hass_to_homekit
|
||||||
|
)
|
||||||
|
self._state_updates += 1
|
||||||
|
|
||||||
|
self._update_state(new_state)
|
||||||
|
|
||||||
|
def _update_state(self, new_state):
|
||||||
|
"""Update state without rechecking the device features."""
|
||||||
features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
# Update target operation mode FIRST
|
# Update target operation mode FIRST
|
||||||
hvac_mode = new_state.state
|
hvac_mode = new_state.state
|
||||||
if hvac_mode and hvac_mode in HC_HASS_TO_HOMEKIT:
|
if hvac_mode and hvac_mode in HC_HASS_TO_HOMEKIT:
|
||||||
homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode]
|
homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode]
|
||||||
if self.char_target_heat_cool.value != homekit_hvac_mode:
|
if homekit_hvac_mode in self.hc_homekit_to_hass:
|
||||||
self.char_target_heat_cool.set_value(homekit_hvac_mode)
|
if self.char_target_heat_cool.value != homekit_hvac_mode:
|
||||||
|
self.char_target_heat_cool.set_value(homekit_hvac_mode)
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Cannot map hvac target mode: %s to homekit as only %s modes are supported",
|
||||||
|
hvac_mode,
|
||||||
|
self.hc_homekit_to_hass,
|
||||||
|
)
|
||||||
|
|
||||||
# Set current operation mode for supported thermostats
|
# Set current operation mode for supported thermostats
|
||||||
hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION)
|
hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION)
|
||||||
@ -444,13 +488,13 @@ class Thermostat(HomeAccessory):
|
|||||||
# even if the device does not support it
|
# even if the device does not support it
|
||||||
hc_hvac_mode = self.char_target_heat_cool.value
|
hc_hvac_mode = self.char_target_heat_cool.value
|
||||||
if hc_hvac_mode == HC_HEAT_COOL_HEAT:
|
if hc_hvac_mode == HC_HEAT_COOL_HEAT:
|
||||||
target_temp = self._temperature_to_homekit(
|
temp_low = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
|
||||||
new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
|
if isinstance(temp_low, (int, float)):
|
||||||
)
|
target_temp = self._temperature_to_homekit(temp_low)
|
||||||
elif hc_hvac_mode == HC_HEAT_COOL_COOL:
|
elif hc_hvac_mode == HC_HEAT_COOL_COOL:
|
||||||
target_temp = self._temperature_to_homekit(
|
temp_high = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
|
||||||
new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
|
if isinstance(temp_high, (int, float)):
|
||||||
)
|
target_temp = self._temperature_to_homekit(temp_high)
|
||||||
if target_temp and self.char_target_temp.value != target_temp:
|
if target_temp and self.char_target_temp.value != target_temp:
|
||||||
self.char_target_temp.set_value(target_temp)
|
self.char_target_temp.set_value(target_temp)
|
||||||
|
|
||||||
|
@ -128,24 +128,20 @@ class IPPEntity(Entity):
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
entry_id: str,
|
entry_id: str,
|
||||||
|
device_id: str,
|
||||||
coordinator: IPPDataUpdateCoordinator,
|
coordinator: IPPDataUpdateCoordinator,
|
||||||
name: str,
|
name: str,
|
||||||
icon: str,
|
icon: str,
|
||||||
enabled_default: bool = True,
|
enabled_default: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the IPP entity."""
|
"""Initialize the IPP entity."""
|
||||||
self._device_id = None
|
self._device_id = device_id
|
||||||
self._enabled_default = enabled_default
|
self._enabled_default = enabled_default
|
||||||
self._entry_id = entry_id
|
self._entry_id = entry_id
|
||||||
self._icon = icon
|
self._icon = icon
|
||||||
self._name = name
|
self._name = name
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
|
|
||||||
if coordinator.data.info.uuid is not None:
|
|
||||||
self._device_id = coordinator.data.info.uuid
|
|
||||||
elif coordinator.data.info.serial is not None:
|
|
||||||
self._device_id = coordinator.data.info.serial
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the entity."""
|
"""Return the name of the entity."""
|
||||||
|
@ -32,13 +32,21 @@ async def async_setup_entry(
|
|||||||
"""Set up IPP sensor based on a config entry."""
|
"""Set up IPP sensor based on a config entry."""
|
||||||
coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
|
# config flow sets this to either UUID, serial number or None
|
||||||
|
unique_id = entry.unique_id
|
||||||
|
|
||||||
|
if unique_id is None:
|
||||||
|
unique_id = entry.entry_id
|
||||||
|
|
||||||
sensors = []
|
sensors = []
|
||||||
|
|
||||||
sensors.append(IPPPrinterSensor(entry.entry_id, coordinator))
|
sensors.append(IPPPrinterSensor(entry.entry_id, unique_id, coordinator))
|
||||||
sensors.append(IPPUptimeSensor(entry.entry_id, coordinator))
|
sensors.append(IPPUptimeSensor(entry.entry_id, unique_id, coordinator))
|
||||||
|
|
||||||
for marker_index in range(len(coordinator.data.markers)):
|
for marker_index in range(len(coordinator.data.markers)):
|
||||||
sensors.append(IPPMarkerSensor(entry.entry_id, coordinator, marker_index))
|
sensors.append(
|
||||||
|
IPPMarkerSensor(entry.entry_id, unique_id, coordinator, marker_index)
|
||||||
|
)
|
||||||
|
|
||||||
async_add_entities(sensors, True)
|
async_add_entities(sensors, True)
|
||||||
|
|
||||||
@ -52,6 +60,7 @@ class IPPSensor(IPPEntity):
|
|||||||
coordinator: IPPDataUpdateCoordinator,
|
coordinator: IPPDataUpdateCoordinator,
|
||||||
enabled_default: bool = True,
|
enabled_default: bool = True,
|
||||||
entry_id: str,
|
entry_id: str,
|
||||||
|
unique_id: str,
|
||||||
icon: str,
|
icon: str,
|
||||||
key: str,
|
key: str,
|
||||||
name: str,
|
name: str,
|
||||||
@ -62,13 +71,12 @@ class IPPSensor(IPPEntity):
|
|||||||
self._key = key
|
self._key = key
|
||||||
self._unique_id = None
|
self._unique_id = None
|
||||||
|
|
||||||
if coordinator.data.info.uuid is not None:
|
if unique_id is not None:
|
||||||
self._unique_id = f"{coordinator.data.info.uuid}_{key}"
|
self._unique_id = f"{unique_id}_{key}"
|
||||||
elif coordinator.data.info.serial is not None:
|
|
||||||
self._unique_id = f"{coordinator.data.info.serial}_{key}"
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
entry_id=entry_id,
|
entry_id=entry_id,
|
||||||
|
device_id=unique_id,
|
||||||
coordinator=coordinator,
|
coordinator=coordinator,
|
||||||
name=name,
|
name=name,
|
||||||
icon=icon,
|
icon=icon,
|
||||||
@ -90,7 +98,11 @@ class IPPMarkerSensor(IPPSensor):
|
|||||||
"""Defines an IPP marker sensor."""
|
"""Defines an IPP marker sensor."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, entry_id: str, coordinator: IPPDataUpdateCoordinator, marker_index: int
|
self,
|
||||||
|
entry_id: str,
|
||||||
|
unique_id: str,
|
||||||
|
coordinator: IPPDataUpdateCoordinator,
|
||||||
|
marker_index: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize IPP marker sensor."""
|
"""Initialize IPP marker sensor."""
|
||||||
self.marker_index = marker_index
|
self.marker_index = marker_index
|
||||||
@ -98,6 +110,7 @@ class IPPMarkerSensor(IPPSensor):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
coordinator=coordinator,
|
coordinator=coordinator,
|
||||||
entry_id=entry_id,
|
entry_id=entry_id,
|
||||||
|
unique_id=unique_id,
|
||||||
icon="mdi:water",
|
icon="mdi:water",
|
||||||
key=f"marker_{marker_index}",
|
key=f"marker_{marker_index}",
|
||||||
name=f"{coordinator.data.info.name} {coordinator.data.markers[marker_index].name}",
|
name=f"{coordinator.data.info.name} {coordinator.data.markers[marker_index].name}",
|
||||||
@ -133,11 +146,14 @@ class IPPMarkerSensor(IPPSensor):
|
|||||||
class IPPPrinterSensor(IPPSensor):
|
class IPPPrinterSensor(IPPSensor):
|
||||||
"""Defines an IPP printer sensor."""
|
"""Defines an IPP printer sensor."""
|
||||||
|
|
||||||
def __init__(self, entry_id: str, coordinator: IPPDataUpdateCoordinator) -> None:
|
def __init__(
|
||||||
|
self, entry_id: str, unique_id: str, coordinator: IPPDataUpdateCoordinator
|
||||||
|
) -> None:
|
||||||
"""Initialize IPP printer sensor."""
|
"""Initialize IPP printer sensor."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
coordinator=coordinator,
|
coordinator=coordinator,
|
||||||
entry_id=entry_id,
|
entry_id=entry_id,
|
||||||
|
unique_id=unique_id,
|
||||||
icon="mdi:printer",
|
icon="mdi:printer",
|
||||||
key="printer",
|
key="printer",
|
||||||
name=coordinator.data.info.name,
|
name=coordinator.data.info.name,
|
||||||
@ -166,12 +182,15 @@ class IPPPrinterSensor(IPPSensor):
|
|||||||
class IPPUptimeSensor(IPPSensor):
|
class IPPUptimeSensor(IPPSensor):
|
||||||
"""Defines a IPP uptime sensor."""
|
"""Defines a IPP uptime sensor."""
|
||||||
|
|
||||||
def __init__(self, entry_id: str, coordinator: IPPDataUpdateCoordinator) -> None:
|
def __init__(
|
||||||
|
self, entry_id: str, unique_id: str, coordinator: IPPDataUpdateCoordinator
|
||||||
|
) -> None:
|
||||||
"""Initialize IPP uptime sensor."""
|
"""Initialize IPP uptime sensor."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
coordinator=coordinator,
|
coordinator=coordinator,
|
||||||
enabled_default=False,
|
enabled_default=False,
|
||||||
entry_id=entry_id,
|
entry_id=entry_id,
|
||||||
|
unique_id=unique_id,
|
||||||
icon="mdi:clock-outline",
|
icon="mdi:clock-outline",
|
||||||
key="uptime",
|
key="uptime",
|
||||||
name=f"{coordinator.data.info.name} Uptime",
|
name=f"{coordinator.data.info.name} Uptime",
|
||||||
|
@ -88,6 +88,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
async def async_step_import(self, user_input):
|
async def async_step_import(self, user_input):
|
||||||
"""Handle import."""
|
"""Handle import."""
|
||||||
|
for entry in self._async_current_entries():
|
||||||
|
if entry.data[CONF_USERNAME] == user_input[CONF_USERNAME]:
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
return await self.async_step_user(user_input)
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "nexia",
|
"domain": "nexia",
|
||||||
"name": "Nexia",
|
"name": "Nexia",
|
||||||
"requirements": ["nexia==0.9.2"],
|
"requirements": ["nexia==0.9.3"],
|
||||||
"codeowners": ["@ryannazaretian", "@bdraco"],
|
"codeowners": ["@ryannazaretian", "@bdraco"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||||
"config_flow": true
|
"config_flow": true
|
||||||
|
@ -9,9 +9,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from sqlalchemy import create_engine, exc, select
|
from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select
|
||||||
from sqlalchemy.engine import Engine
|
|
||||||
from sqlalchemy.event import listens_for
|
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -488,15 +486,13 @@ class Recorder(threading.Thread):
|
|||||||
"""Ensure database is ready to fly."""
|
"""Ensure database is ready to fly."""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
# pylint: disable=unused-variable
|
def setup_recorder_connection(dbapi_connection, connection_record):
|
||||||
@listens_for(Engine, "connect")
|
|
||||||
def setup_connection(dbapi_connection, connection_record):
|
|
||||||
"""Dbapi specific connection settings."""
|
"""Dbapi specific connection settings."""
|
||||||
|
|
||||||
# We do not import sqlite3 here so mysql/other
|
# We do not import sqlite3 here so mysql/other
|
||||||
# users do not have to pay for it to be loaded in
|
# users do not have to pay for it to be loaded in
|
||||||
# memory
|
# memory
|
||||||
if self.db_url == "sqlite://" or ":memory:" in self.db_url:
|
if self.db_url.startswith("sqlite://"):
|
||||||
old_isolation = dbapi_connection.isolation_level
|
old_isolation = dbapi_connection.isolation_level
|
||||||
dbapi_connection.isolation_level = None
|
dbapi_connection.isolation_level = None
|
||||||
cursor = dbapi_connection.cursor()
|
cursor = dbapi_connection.cursor()
|
||||||
@ -519,6 +515,9 @@ class Recorder(threading.Thread):
|
|||||||
self.engine.dispose()
|
self.engine.dispose()
|
||||||
|
|
||||||
self.engine = create_engine(self.db_url, **kwargs)
|
self.engine = create_engine(self.db_url, **kwargs)
|
||||||
|
|
||||||
|
sqlalchemy_event.listen(self.engine, "connect", setup_recorder_connection)
|
||||||
|
|
||||||
Base.metadata.create_all(self.engine)
|
Base.metadata.create_all(self.engine)
|
||||||
self.get_session = scoped_session(sessionmaker(bind=self.engine))
|
self.get_session = scoped_session(sessionmaker(bind=self.engine))
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ from .const import (
|
|||||||
TOKEN_REFRESH_INTERVAL,
|
TOKEN_REFRESH_INTERVAL,
|
||||||
)
|
)
|
||||||
from .smartapp import (
|
from .smartapp import (
|
||||||
|
format_unique_id,
|
||||||
setup_smartapp,
|
setup_smartapp,
|
||||||
setup_smartapp_endpoint,
|
setup_smartapp_endpoint,
|
||||||
smartapp_sync_subscriptions,
|
smartapp_sync_subscriptions,
|
||||||
@ -76,6 +77,15 @@ async def async_migrate_entry(hass: HomeAssistantType, entry: ConfigEntry):
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
|
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
|
||||||
"""Initialize config entry which represents an installed SmartApp."""
|
"""Initialize config entry which represents an installed SmartApp."""
|
||||||
|
# For backwards compat
|
||||||
|
if entry.unique_id is None:
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry,
|
||||||
|
unique_id=format_unique_id(
|
||||||
|
entry.data[CONF_APP_ID], entry.data[CONF_LOCATION_ID]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if not validate_webhook_requirements(hass):
|
if not validate_webhook_requirements(hass):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"The 'base_url' of the 'http' integration must be configured and start with 'https://'"
|
"The 'base_url' of the 'http' integration must be configured and start with 'https://'"
|
||||||
|
@ -7,7 +7,7 @@ from pysmartthings.installedapp import format_install_url
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_FORBIDDEN
|
from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_FORBIDDEN, HTTP_UNAUTHORIZED
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
@ -26,6 +26,7 @@ from .const import (
|
|||||||
from .smartapp import (
|
from .smartapp import (
|
||||||
create_app,
|
create_app,
|
||||||
find_app,
|
find_app,
|
||||||
|
format_unique_id,
|
||||||
get_webhook_url,
|
get_webhook_url,
|
||||||
setup_smartapp,
|
setup_smartapp,
|
||||||
setup_smartapp_endpoint,
|
setup_smartapp_endpoint,
|
||||||
@ -138,7 +139,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
return self._show_step_pat(errors)
|
return self._show_step_pat(errors)
|
||||||
except ClientResponseError as ex:
|
except ClientResponseError as ex:
|
||||||
if ex.status == 401:
|
if ex.status == HTTP_UNAUTHORIZED:
|
||||||
errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
|
errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Unauthorized error received setting up SmartApp", exc_info=True
|
"Unauthorized error received setting up SmartApp", exc_info=True
|
||||||
@ -183,6 +184,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.location_id = user_input[CONF_LOCATION_ID]
|
self.location_id = user_input[CONF_LOCATION_ID]
|
||||||
|
await self.async_set_unique_id(format_unique_id(self.app_id, self.location_id))
|
||||||
return await self.async_step_authorize()
|
return await self.async_step_authorize()
|
||||||
|
|
||||||
async def async_step_authorize(self, user_input=None):
|
async def async_step_authorize(self, user_input=None):
|
||||||
|
@ -39,7 +39,6 @@ from .const import (
|
|||||||
CONF_CLOUDHOOK_URL,
|
CONF_CLOUDHOOK_URL,
|
||||||
CONF_INSTALLED_APP_ID,
|
CONF_INSTALLED_APP_ID,
|
||||||
CONF_INSTANCE_ID,
|
CONF_INSTANCE_ID,
|
||||||
CONF_LOCATION_ID,
|
|
||||||
CONF_REFRESH_TOKEN,
|
CONF_REFRESH_TOKEN,
|
||||||
DATA_BROKERS,
|
DATA_BROKERS,
|
||||||
DATA_MANAGER,
|
DATA_MANAGER,
|
||||||
@ -53,6 +52,11 @@ from .const import (
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def format_unique_id(app_id: str, location_id: str) -> str:
|
||||||
|
"""Format the unique id for a config entry."""
|
||||||
|
return f"{app_id}_{location_id}"
|
||||||
|
|
||||||
|
|
||||||
async def find_app(hass: HomeAssistantType, api):
|
async def find_app(hass: HomeAssistantType, api):
|
||||||
"""Find an existing SmartApp for this installation of hass."""
|
"""Find an existing SmartApp for this installation of hass."""
|
||||||
apps = await api.apps()
|
apps = await api.apps()
|
||||||
@ -366,13 +370,20 @@ async def smartapp_sync_subscriptions(
|
|||||||
_LOGGER.debug("Subscriptions for app '%s' are up-to-date", installed_app_id)
|
_LOGGER.debug("Subscriptions for app '%s' are up-to-date", installed_app_id)
|
||||||
|
|
||||||
|
|
||||||
async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
async def _continue_flow(
|
||||||
"""Handle a SmartApp installation and continue the config flow."""
|
hass: HomeAssistantType,
|
||||||
|
app_id: str,
|
||||||
|
location_id: str,
|
||||||
|
installed_app_id: str,
|
||||||
|
refresh_token: str,
|
||||||
|
):
|
||||||
|
"""Continue a config flow if one is in progress for the specific installed app."""
|
||||||
|
unique_id = format_unique_id(app_id, location_id)
|
||||||
flow = next(
|
flow = next(
|
||||||
(
|
(
|
||||||
flow
|
flow
|
||||||
for flow in hass.config_entries.flow.async_progress()
|
for flow in hass.config_entries.flow.async_progress()
|
||||||
if flow["handler"] == DOMAIN
|
if flow["handler"] == DOMAIN and flow["context"]["unique_id"] == unique_id
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@ -380,18 +391,23 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
|||||||
await hass.config_entries.flow.async_configure(
|
await hass.config_entries.flow.async_configure(
|
||||||
flow["flow_id"],
|
flow["flow_id"],
|
||||||
{
|
{
|
||||||
CONF_INSTALLED_APP_ID: req.installed_app_id,
|
CONF_INSTALLED_APP_ID: installed_app_id,
|
||||||
CONF_LOCATION_ID: req.location_id,
|
CONF_REFRESH_TOKEN: refresh_token,
|
||||||
CONF_REFRESH_TOKEN: req.refresh_token,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Continued config flow '%s' for SmartApp '%s' under parent app '%s'",
|
"Continued config flow '%s' for SmartApp '%s' under parent app '%s'",
|
||||||
flow["flow_id"],
|
flow["flow_id"],
|
||||||
req.installed_app_id,
|
installed_app_id,
|
||||||
app.app_id,
|
app_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
||||||
|
"""Handle a SmartApp installation and continue the config flow."""
|
||||||
|
await _continue_flow(
|
||||||
|
hass, app.app_id, req.location_id, req.installed_app_id, req.refresh_token
|
||||||
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Installed SmartApp '%s' under parent app '%s'",
|
"Installed SmartApp '%s' under parent app '%s'",
|
||||||
req.installed_app_id,
|
req.installed_app_id,
|
||||||
@ -420,30 +436,9 @@ async def smartapp_update(hass: HomeAssistantType, req, resp, app):
|
|||||||
app.app_id,
|
app.app_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
flow = next(
|
await _continue_flow(
|
||||||
(
|
hass, app.app_id, req.location_id, req.installed_app_id, req.refresh_token
|
||||||
flow
|
|
||||||
for flow in hass.config_entries.flow.async_progress()
|
|
||||||
if flow["handler"] == DOMAIN
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
if flow is not None:
|
|
||||||
await hass.config_entries.flow.async_configure(
|
|
||||||
flow["flow_id"],
|
|
||||||
{
|
|
||||||
CONF_INSTALLED_APP_ID: req.installed_app_id,
|
|
||||||
CONF_LOCATION_ID: req.location_id,
|
|
||||||
CONF_REFRESH_TOKEN: req.refresh_token,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Continued config flow '%s' for SmartApp '%s' under parent app '%s'",
|
|
||||||
flow["flow_id"],
|
|
||||||
req.installed_app_id,
|
|
||||||
app.app_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Updated SmartApp '%s' under parent app '%s'", req.installed_app_id, app.app_id
|
"Updated SmartApp '%s' under parent app '%s'", req.installed_app_id, app.app_id
|
||||||
)
|
)
|
||||||
|
@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigType) -> bool:
|
|||||||
)
|
)
|
||||||
hass.data.setdefault(DATA_TOON_CLIENT, {})[entry.entry_id] = toon
|
hass.data.setdefault(DATA_TOON_CLIENT, {})[entry.entry_id] = toon
|
||||||
|
|
||||||
toon_data = ToonData(hass, entry, toon)
|
toon_data = await hass.async_add_executor_job(ToonData, hass, entry, toon)
|
||||||
hass.data.setdefault(DATA_TOON, {})[entry.entry_id] = toon_data
|
hass.data.setdefault(DATA_TOON, {})[entry.entry_id] = toon_data
|
||||||
async_track_time_interval(hass, toon_data.update, conf[CONF_SCAN_INTERVAL])
|
async_track_time_interval(hass, toon_data.update, conf[CONF_SCAN_INTERVAL])
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ async def async_setup_tv_finalize(hass, config, conf, client):
|
|||||||
|
|
||||||
if client.connection is None:
|
if client.connection is None:
|
||||||
async_call_later(hass, 60, async_load_platforms)
|
async_call_later(hass, 60, async_load_platforms)
|
||||||
_LOGGER.warning(
|
_LOGGER.debug(
|
||||||
"No connection could be made with host %s, retrying in 60 seconds",
|
"No connection could be made with host %s, retrying in 60 seconds",
|
||||||
conf.get(CONF_HOST),
|
conf.get(CONF_HOST),
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Constants used by Home Assistant components."""
|
"""Constants used by Home Assistant components."""
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 109
|
MINOR_VERSION = 109
|
||||||
PATCH_VERSION = "0"
|
PATCH_VERSION = "1"
|
||||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||||
|
@ -931,7 +931,7 @@ netdisco==2.6.0
|
|||||||
neurio==0.3.1
|
neurio==0.3.1
|
||||||
|
|
||||||
# homeassistant.components.nexia
|
# homeassistant.components.nexia
|
||||||
nexia==0.9.2
|
nexia==0.9.3
|
||||||
|
|
||||||
# homeassistant.components.nextcloud
|
# homeassistant.components.nextcloud
|
||||||
nextcloudmonitor==1.1.0
|
nextcloudmonitor==1.1.0
|
||||||
|
@ -366,7 +366,7 @@ nessclient==0.9.15
|
|||||||
netdisco==2.6.0
|
netdisco==2.6.0
|
||||||
|
|
||||||
# homeassistant.components.nexia
|
# homeassistant.components.nexia
|
||||||
nexia==0.9.2
|
nexia==0.9.3
|
||||||
|
|
||||||
# homeassistant.components.nsw_fuel_station
|
# homeassistant.components.nsw_fuel_station
|
||||||
nsw-fuel-api-client==1.0.10
|
nsw-fuel-api-client==1.0.10
|
||||||
|
@ -363,9 +363,30 @@ async def test_missing_linked_battery_sensor(hass, hk_driver, caplog):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert not acc.linked_battery_sensor
|
assert not acc.linked_battery_sensor
|
||||||
assert not hasattr(acc, "_char_battery")
|
assert acc._char_battery is None
|
||||||
assert not hasattr(acc, "_char_low_battery")
|
assert acc._char_low_battery is None
|
||||||
assert not hasattr(acc, "_char_charging")
|
assert acc._char_charging is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_battery_appears_after_startup(hass, hk_driver, caplog):
|
||||||
|
"""Test battery level appears after homekit is started."""
|
||||||
|
entity_id = "homekit.accessory"
|
||||||
|
hass.states.async_set(entity_id, None, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
acc = HomeAccessory(
|
||||||
|
hass, hk_driver, "Accessory without battery", entity_id, 2, None
|
||||||
|
)
|
||||||
|
acc.update_state = lambda x: None
|
||||||
|
assert acc._char_battery is None
|
||||||
|
|
||||||
|
await acc.run_handler()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc._char_battery is None
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc._char_battery is None
|
||||||
|
|
||||||
|
|
||||||
async def test_call_service(hass, hk_driver, events):
|
async def test_call_service(hass, hk_driver, events):
|
||||||
|
@ -419,8 +419,7 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events):
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
acc.speed_mapping.speed_to_states.assert_called_with(42)
|
acc.speed_mapping.speed_to_states.assert_called_with(42)
|
||||||
assert call_turn_on
|
assert not call_turn_on
|
||||||
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
|
|
||||||
assert call_set_speed[0]
|
assert call_set_speed[0]
|
||||||
assert call_set_speed[0].data[ATTR_ENTITY_ID] == entity_id
|
assert call_set_speed[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
assert call_set_speed[0].data[ATTR_SPEED] == "ludicrous"
|
assert call_set_speed[0].data[ATTR_SPEED] == "ludicrous"
|
||||||
@ -430,11 +429,11 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events):
|
|||||||
assert call_set_direction[0]
|
assert call_set_direction[0]
|
||||||
assert call_set_direction[0].data[ATTR_ENTITY_ID] == entity_id
|
assert call_set_direction[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
assert call_set_direction[0].data[ATTR_DIRECTION] == DIRECTION_REVERSE
|
assert call_set_direction[0].data[ATTR_DIRECTION] == DIRECTION_REVERSE
|
||||||
assert len(events) == 4
|
assert len(events) == 3
|
||||||
|
|
||||||
assert events[1].data[ATTR_VALUE] is True
|
assert events[0].data[ATTR_VALUE] is True
|
||||||
assert events[2].data[ATTR_VALUE] == DIRECTION_REVERSE
|
assert events[1].data[ATTR_VALUE] == DIRECTION_REVERSE
|
||||||
assert events[3].data[ATTR_VALUE] == "ludicrous"
|
assert events[2].data[ATTR_VALUE] == "ludicrous"
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
entity_id,
|
entity_id,
|
||||||
@ -482,7 +481,7 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events):
|
|||||||
# and we set a fan speed
|
# and we set a fan speed
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
acc.speed_mapping.speed_to_states.assert_called_with(42)
|
acc.speed_mapping.speed_to_states.assert_called_with(42)
|
||||||
assert len(events) == 7
|
assert len(events) == 6
|
||||||
assert call_set_speed[1]
|
assert call_set_speed[1]
|
||||||
assert call_set_speed[1].data[ATTR_ENTITY_ID] == entity_id
|
assert call_set_speed[1].data[ATTR_ENTITY_ID] == entity_id
|
||||||
assert call_set_speed[1].data[ATTR_SPEED] == "ludicrous"
|
assert call_set_speed[1].data[ATTR_SPEED] == "ludicrous"
|
||||||
@ -526,7 +525,7 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events):
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(events) == 8
|
assert len(events) == 7
|
||||||
assert call_turn_off
|
assert call_turn_off
|
||||||
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
|
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
assert len(call_set_speed) == 2
|
assert len(call_set_speed) == 2
|
||||||
|
@ -43,7 +43,6 @@ from homeassistant.components.homekit.const import (
|
|||||||
PROP_MIN_STEP,
|
PROP_MIN_STEP,
|
||||||
PROP_MIN_VALUE,
|
PROP_MIN_VALUE,
|
||||||
)
|
)
|
||||||
from homeassistant.components.homekit.type_thermostats import HC_MIN_TEMP
|
|
||||||
from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER
|
from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
@ -116,7 +115,7 @@ async def test_thermostat(hass, hk_driver, cls, events):
|
|||||||
assert acc.char_current_humidity is None
|
assert acc.char_current_humidity is None
|
||||||
|
|
||||||
assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
assert acc.char_target_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
|
assert acc.char_target_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1
|
assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -126,6 +125,14 @@ async def test_thermostat(hass, hk_driver, cls, events):
|
|||||||
ATTR_TEMPERATURE: 22.2,
|
ATTR_TEMPERATURE: 22.2,
|
||||||
ATTR_CURRENT_TEMPERATURE: 17.8,
|
ATTR_CURRENT_TEMPERATURE: 17.8,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -142,6 +149,14 @@ async def test_thermostat(hass, hk_driver, cls, events):
|
|||||||
ATTR_TEMPERATURE: 22.0,
|
ATTR_TEMPERATURE: 22.0,
|
||||||
ATTR_CURRENT_TEMPERATURE: 23.0,
|
ATTR_CURRENT_TEMPERATURE: 23.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -158,6 +173,14 @@ async def test_thermostat(hass, hk_driver, cls, events):
|
|||||||
ATTR_TEMPERATURE: 20.0,
|
ATTR_TEMPERATURE: 20.0,
|
||||||
ATTR_CURRENT_TEMPERATURE: 25.0,
|
ATTR_CURRENT_TEMPERATURE: 25.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -202,6 +225,14 @@ async def test_thermostat(hass, hk_driver, cls, events):
|
|||||||
ATTR_TEMPERATURE: 22.0,
|
ATTR_TEMPERATURE: 22.0,
|
||||||
ATTR_CURRENT_TEMPERATURE: 18.0,
|
ATTR_CURRENT_TEMPERATURE: 18.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -218,6 +249,14 @@ async def test_thermostat(hass, hk_driver, cls, events):
|
|||||||
ATTR_TEMPERATURE: 22.0,
|
ATTR_TEMPERATURE: 22.0,
|
||||||
ATTR_CURRENT_TEMPERATURE: 25.0,
|
ATTR_CURRENT_TEMPERATURE: 25.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -234,6 +273,14 @@ async def test_thermostat(hass, hk_driver, cls, events):
|
|||||||
ATTR_TEMPERATURE: 22.0,
|
ATTR_TEMPERATURE: 22.0,
|
||||||
ATTR_CURRENT_TEMPERATURE: 22.0,
|
ATTR_CURRENT_TEMPERATURE: 22.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -250,6 +297,14 @@ async def test_thermostat(hass, hk_driver, cls, events):
|
|||||||
ATTR_TEMPERATURE: 22.0,
|
ATTR_TEMPERATURE: 22.0,
|
||||||
ATTR_CURRENT_TEMPERATURE: 22.0,
|
ATTR_CURRENT_TEMPERATURE: 22.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_FAN,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_FAN,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -369,7 +424,15 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
|
|||||||
HVAC_MODE_OFF,
|
HVAC_MODE_OFF,
|
||||||
{
|
{
|
||||||
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
|
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
|
||||||
| SUPPORT_TARGET_TEMPERATURE_RANGE
|
| SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -383,10 +446,10 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
|
|||||||
assert acc.char_heating_thresh_temp.value == 19.0
|
assert acc.char_heating_thresh_temp.value == 19.0
|
||||||
|
|
||||||
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -397,6 +460,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
|
|||||||
ATTR_TARGET_TEMP_LOW: 20.0,
|
ATTR_TARGET_TEMP_LOW: 20.0,
|
||||||
ATTR_CURRENT_TEMPERATURE: 18.0,
|
ATTR_CURRENT_TEMPERATURE: 18.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -415,6 +486,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
|
|||||||
ATTR_TARGET_TEMP_LOW: 19.0,
|
ATTR_TARGET_TEMP_LOW: 19.0,
|
||||||
ATTR_CURRENT_TEMPERATURE: 24.0,
|
ATTR_CURRENT_TEMPERATURE: 24.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -433,6 +512,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events):
|
|||||||
ATTR_TARGET_TEMP_LOW: 19.0,
|
ATTR_TARGET_TEMP_LOW: 19.0,
|
||||||
ATTR_CURRENT_TEMPERATURE: 21.0,
|
ATTR_CURRENT_TEMPERATURE: 21.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -1094,10 +1181,10 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e
|
|||||||
assert acc.char_heating_thresh_temp.value == 19.0
|
assert acc.char_heating_thresh_temp.value == 19.0
|
||||||
|
|
||||||
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == HC_MIN_TEMP
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -1109,6 +1196,14 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e
|
|||||||
ATTR_CURRENT_TEMPERATURE: 18.0,
|
ATTR_CURRENT_TEMPERATURE: 18.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
||||||
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
|
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -1128,6 +1223,14 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e
|
|||||||
ATTR_CURRENT_TEMPERATURE: 24.0,
|
ATTR_CURRENT_TEMPERATURE: 24.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_COOL,
|
||||||
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
|
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -1147,6 +1250,14 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e
|
|||||||
ATTR_CURRENT_TEMPERATURE: 21.0,
|
ATTR_CURRENT_TEMPERATURE: 21.0,
|
||||||
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
|
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
|
||||||
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
|
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
HVAC_MODE_FAN_ONLY,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -1400,3 +1511,109 @@ async def test_water_heater_restore(hass, hk_driver, cls, events):
|
|||||||
"Heat",
|
"Heat",
|
||||||
"Off",
|
"Off",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls, events):
|
||||||
|
"""Test if a thermostat that is not ready when we first see it."""
|
||||||
|
entity_id = "climate.test"
|
||||||
|
|
||||||
|
# support_auto = True
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
HVAC_MODE_OFF,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
|
||||||
|
| SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
|
ATTR_HVAC_MODES: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
|
||||||
|
hk_driver.add_accessory(acc)
|
||||||
|
|
||||||
|
await acc.run_handler()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.char_cooling_thresh_temp.value == 23.0
|
||||||
|
assert acc.char_heating_thresh_temp.value == 19.0
|
||||||
|
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
|
||||||
|
assert acc.char_target_heat_cool.value == 0
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
{
|
||||||
|
ATTR_TARGET_TEMP_HIGH: 22.0,
|
||||||
|
ATTR_TARGET_TEMP_LOW: 20.0,
|
||||||
|
ATTR_CURRENT_TEMPERATURE: 18.0,
|
||||||
|
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
||||||
|
ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODE_AUTO],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_heating_thresh_temp.value == 20.0
|
||||||
|
assert acc.char_cooling_thresh_temp.value == 22.0
|
||||||
|
assert acc.char_current_heat_cool.value == 1
|
||||||
|
assert acc.char_target_heat_cool.value == 3
|
||||||
|
assert acc.char_current_temp.value == 18.0
|
||||||
|
assert acc.char_display_units.value == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events):
|
||||||
|
"""Test if a thermostat that is not ready when we first see it that actually does not have off."""
|
||||||
|
entity_id = "climate.test"
|
||||||
|
|
||||||
|
# support_auto = True
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
HVAC_MODE_COOL,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
|
||||||
|
| SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
|
ATTR_HVAC_MODES: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
|
||||||
|
hk_driver.add_accessory(acc)
|
||||||
|
|
||||||
|
await acc.run_handler()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.char_cooling_thresh_temp.value == 23.0
|
||||||
|
assert acc.char_heating_thresh_temp.value == 19.0
|
||||||
|
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
|
||||||
|
assert acc.char_target_heat_cool.value == 2
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
HVAC_MODE_HEAT_COOL,
|
||||||
|
{
|
||||||
|
ATTR_TARGET_TEMP_HIGH: 22.0,
|
||||||
|
ATTR_TARGET_TEMP_LOW: 20.0,
|
||||||
|
ATTR_CURRENT_TEMPERATURE: 18.0,
|
||||||
|
ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT,
|
||||||
|
ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_heating_thresh_temp.value == 20.0
|
||||||
|
assert acc.char_cooling_thresh_temp.value == 22.0
|
||||||
|
assert acc.char_current_heat_cool.value == 1
|
||||||
|
assert acc.char_target_heat_cool.value == 3
|
||||||
|
assert acc.char_current_temp.value == 18.0
|
||||||
|
assert acc.char_display_units.value == 0
|
||||||
|
@ -62,7 +62,11 @@ def load_fixture_binary(filename):
|
|||||||
|
|
||||||
|
|
||||||
async def init_integration(
|
async def init_integration(
|
||||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False,
|
hass: HomeAssistant,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
skip_setup: bool = False,
|
||||||
|
uuid: str = "cfe92100-67c4-11d4-a45f-f8d027761251",
|
||||||
|
unique_id: str = "cfe92100-67c4-11d4-a45f-f8d027761251",
|
||||||
) -> MockConfigEntry:
|
) -> MockConfigEntry:
|
||||||
"""Set up the IPP integration in Home Assistant."""
|
"""Set up the IPP integration in Home Assistant."""
|
||||||
fixture = "ipp/get-printer-attributes.bin"
|
fixture = "ipp/get-printer-attributes.bin"
|
||||||
@ -74,14 +78,14 @@ async def init_integration(
|
|||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
unique_id="cfe92100-67c4-11d4-a45f-f8d027761251",
|
unique_id=unique_id,
|
||||||
data={
|
data={
|
||||||
CONF_HOST: "192.168.1.31",
|
CONF_HOST: "192.168.1.31",
|
||||||
CONF_PORT: 631,
|
CONF_PORT: 631,
|
||||||
CONF_SSL: False,
|
CONF_SSL: False,
|
||||||
CONF_VERIFY_SSL: True,
|
CONF_VERIFY_SSL: True,
|
||||||
CONF_BASE_PATH: "/ipp/print",
|
CONF_BASE_PATH: "/ipp/print",
|
||||||
CONF_UUID: "cfe92100-67c4-11d4-a45f-f8d027761251",
|
CONF_UUID: uuid,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -94,3 +94,15 @@ async def test_disabled_by_default_sensors(
|
|||||||
assert entry
|
assert entry
|
||||||
assert entry.disabled
|
assert entry.disabled
|
||||||
assert entry.disabled_by == "integration"
|
assert entry.disabled_by == "integration"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_missing_entry_unique_id(
|
||||||
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
) -> None:
|
||||||
|
"""Test the unique_id of IPP sensor when printer is missing identifiers."""
|
||||||
|
entry = await init_integration(hass, aioclient_mock, uuid=None, unique_id=None)
|
||||||
|
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
|
entity = registry.async_get("sensor.epson_xp_6000_series")
|
||||||
|
assert entity
|
||||||
|
assert entity.unique_id == f"{entry.entry_id}_printer"
|
||||||
|
@ -74,3 +74,44 @@ async def test_form_cannot_connect(hass):
|
|||||||
|
|
||||||
assert result2["type"] == "form"
|
assert result2["type"] == "form"
|
||||||
assert result2["errors"] == {"base": "cannot_connect"}
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_import(hass):
|
||||||
|
"""Test we get the form with import source."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.nexia.config_flow.NexiaHome.get_name",
|
||||||
|
return_value="myhouse",
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.nexia.config_flow.NexiaHome.login",
|
||||||
|
side_effect=MagicMock(),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.nexia.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.nexia.async_setup_entry", return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={CONF_USERNAME: "username", CONF_PASSWORD: "password"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["title"] == "myhouse"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_USERNAME: "username",
|
||||||
|
CONF_PASSWORD: "password",
|
||||||
|
}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={CONF_USERNAME: "username", CONF_PASSWORD: "password"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "abort"
|
||||||
|
assert result2["reason"] == "already_configured"
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,6 @@ from pysmartthings import AppEntity, Capability
|
|||||||
|
|
||||||
from homeassistant.components.smartthings import smartapp
|
from homeassistant.components.smartthings import smartapp
|
||||||
from homeassistant.components.smartthings.const import (
|
from homeassistant.components.smartthings.const import (
|
||||||
CONF_INSTALLED_APP_ID,
|
|
||||||
CONF_LOCATION_ID,
|
|
||||||
CONF_REFRESH_TOKEN,
|
CONF_REFRESH_TOKEN,
|
||||||
DATA_MANAGER,
|
DATA_MANAGER,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -39,36 +37,6 @@ async def test_update_app_updated_needed(hass, app):
|
|||||||
assert mock_app.classifications == app.classifications
|
assert mock_app.classifications == app.classifications
|
||||||
|
|
||||||
|
|
||||||
async def test_smartapp_install_configures_flow(hass):
|
|
||||||
"""Test install event continues an existing flow."""
|
|
||||||
# Arrange
|
|
||||||
flow_id = str(uuid4())
|
|
||||||
flows = [{"flow_id": flow_id, "handler": DOMAIN}]
|
|
||||||
app = Mock()
|
|
||||||
app.app_id = uuid4()
|
|
||||||
request = Mock()
|
|
||||||
request.installed_app_id = str(uuid4())
|
|
||||||
request.auth_token = str(uuid4())
|
|
||||||
request.location_id = str(uuid4())
|
|
||||||
request.refresh_token = str(uuid4())
|
|
||||||
|
|
||||||
# Act
|
|
||||||
with patch.object(
|
|
||||||
hass.config_entries.flow, "async_progress", return_value=flows
|
|
||||||
), patch.object(hass.config_entries.flow, "async_configure") as configure_mock:
|
|
||||||
|
|
||||||
await smartapp.smartapp_install(hass, request, None, app)
|
|
||||||
|
|
||||||
configure_mock.assert_called_once_with(
|
|
||||||
flow_id,
|
|
||||||
{
|
|
||||||
CONF_INSTALLED_APP_ID: request.installed_app_id,
|
|
||||||
CONF_LOCATION_ID: request.location_id,
|
|
||||||
CONF_REFRESH_TOKEN: request.refresh_token,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_smartapp_update_saves_token(
|
async def test_smartapp_update_saves_token(
|
||||||
hass, smartthings_mock, location, device_factory
|
hass, smartthings_mock, location, device_factory
|
||||||
):
|
):
|
||||||
@ -92,36 +60,6 @@ async def test_smartapp_update_saves_token(
|
|||||||
assert entry.data[CONF_REFRESH_TOKEN] == request.refresh_token
|
assert entry.data[CONF_REFRESH_TOKEN] == request.refresh_token
|
||||||
|
|
||||||
|
|
||||||
async def test_smartapp_update_configures_flow(hass):
|
|
||||||
"""Test update event continues an existing flow."""
|
|
||||||
# Arrange
|
|
||||||
flow_id = str(uuid4())
|
|
||||||
flows = [{"flow_id": flow_id, "handler": DOMAIN}]
|
|
||||||
app = Mock()
|
|
||||||
app.app_id = uuid4()
|
|
||||||
request = Mock()
|
|
||||||
request.installed_app_id = str(uuid4())
|
|
||||||
request.auth_token = str(uuid4())
|
|
||||||
request.location_id = str(uuid4())
|
|
||||||
request.refresh_token = str(uuid4())
|
|
||||||
|
|
||||||
# Act
|
|
||||||
with patch.object(
|
|
||||||
hass.config_entries.flow, "async_progress", return_value=flows
|
|
||||||
), patch.object(hass.config_entries.flow, "async_configure") as configure_mock:
|
|
||||||
|
|
||||||
await smartapp.smartapp_update(hass, request, None, app)
|
|
||||||
|
|
||||||
configure_mock.assert_called_once_with(
|
|
||||||
flow_id,
|
|
||||||
{
|
|
||||||
CONF_INSTALLED_APP_ID: request.installed_app_id,
|
|
||||||
CONF_LOCATION_ID: request.location_id,
|
|
||||||
CONF_REFRESH_TOKEN: request.refresh_token,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_smartapp_uninstall(hass, config_entry):
|
async def test_smartapp_uninstall(hass, config_entry):
|
||||||
"""Test the config entry is unloaded when the app is uninstalled."""
|
"""Test the config entry is unloaded when the app is uninstalled."""
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user