Compare commits

...

20 Commits

Author SHA1 Message Date
Franck Nijhof
7a72ada8b2 Bumped version to 0.110.0b4 2020-05-18 22:36:58 +02:00
Bram Kragten
a28646bc24 Updated frontend to 20200518.0 (#35785) 2020-05-18 22:31:35 +02:00
MatsNl
ca4433bd70 Bump Atag dependency to 0.3.1.2 (#35776) 2020-05-18 22:31:29 +02:00
uvjustin
c61bcbf982 Skip forked_daapd ignored entries with empty entry.data (#35772) 2020-05-18 22:31:24 +02:00
Fredrik Erlandsson
e9f398ac28 Fix daikin discovery flow (#35767) 2020-05-18 22:31:19 +02:00
J. Nick Koston
7417b3be66 Handle UPS disconnects in NUT (#35758) 2020-05-18 22:31:14 +02:00
Daniel Høyer Iversen
99afc17b3f Update mill manifest to reflect config flow (#35748) 2020-05-18 22:31:10 +02:00
J. Nick Koston
cc5fc2baa4 Ensure homekit version strings conform to spec (#35741)
HomeKit requires all version strings to be in the
format MAJOR.MINOR.REVISION
2020-05-18 22:31:05 +02:00
Daniel Høyer Iversen
e2f0520028 Upgrade opengarage lib to 0.1.4 (#35729) 2020-05-18 22:31:00 +02:00
uvjustin
5cb1924290 Abort forked-daapd zeroconf flow if version < 27 (#35709)
* Change MediaPlayerDevice to MediaPlayerEntity

* Abort zeroconf if mtd-version < 27.0
2020-05-18 22:30:55 +02:00
Alexei Chetroi
aa176aab07 Bump up ZHA dependencies (#35706) 2020-05-18 22:30:51 +02:00
Franck Nijhof
5695a63e59 Fix handling of additional data in core config storage (#35660) 2020-05-18 22:30:45 +02:00
Franck Nijhof
4a9a004de0 Bumped version to 0.110.0b3 2020-05-16 11:06:22 +02:00
uvjustin
c270d5edcf Change MediaPlayerDevice to MediaPlayerEntity (#35692) 2020-05-16 11:05:15 +02:00
Bram Kragten
cf034ee729 Updated frontend to 20200515.0 (#35677) 2020-05-16 11:05:11 +02:00
Jason Hunter
316d44cf33 ONVIF: Add check around media capabilities (#35667) 2020-05-16 11:05:07 +02:00
Bram Kragten
dbd30d571d Fix caldav event for calendar panel (#35653) 2020-05-16 11:05:03 +02:00
Xiaonan Shen
78c9411dde Bump roombapy to 1.6.1 (#35650)
* Bump roombapy to 1.6.1

* Improve roomba error handling
2020-05-16 11:04:59 +02:00
Glenn Waters
2f999dd77e Update Universal Powerline Bus event name (#35644) 2020-05-16 11:04:55 +02:00
Chris Talkington
e8ee3c7d4d Prevent discovery of IPP printers lacking identifier (#35630) 2020-05-16 11:04:51 +02:00
37 changed files with 295 additions and 193 deletions

View File

@@ -3,6 +3,6 @@
"name": "Atag",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/atag/",
"requirements": ["pyatag==0.3.1.1"],
"requirements": ["pyatag==0.3.1.2"],
"codeowners": ["@MatsNL"]
}

View File

@@ -174,7 +174,7 @@ class WebDavCalendarData:
uid = vevent.uid.value
data = {
"uid": uid,
"title": vevent.summary.value,
"summary": vevent.summary.value,
"start": self.get_hass_date(vevent.dtstart.value),
"end": self.get_hass_date(self.get_end_date(vevent)),
"location": self.get_attr_value(vevent, "location"),

View File

@@ -15,14 +15,6 @@ from .const import CONF_KEY, CONF_UUID, KEY_IP, KEY_MAC, TIMEOUT
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_KEY): str,
vol.Optional(CONF_PASSWORD): str,
}
)
@config_entries.HANDLERS.register("daikin")
class FlowHandler(config_entries.ConfigFlow):
@@ -31,12 +23,26 @@ class FlowHandler(config_entries.ConfigFlow):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def _create_entry(self, host, mac, key=None, uuid=None, password=None):
def __init__(self):
"""Initialize the Daikin config flow."""
self.host = None
@property
def schema(self):
"""Return current schema."""
return vol.Schema(
{
vol.Required(CONF_HOST, default=self.host): str,
vol.Optional(CONF_KEY): str,
vol.Optional(CONF_PASSWORD): str,
}
)
async def _create_entry(self, host, mac, key=None, uuid=None, password=None):
"""Register new entry."""
# Check if mac already is registered
for entry in self._async_current_entries():
if entry.data[KEY_MAC] == mac:
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=host,
@@ -73,31 +79,31 @@ class FlowHandler(config_entries.ConfigFlow):
except asyncio.TimeoutError:
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
data_schema=self.schema,
errors={"base": "device_timeout"},
)
except web_exceptions.HTTPForbidden:
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors={"base": "forbidden"},
step_id="user", data_schema=self.schema, errors={"base": "forbidden"},
)
except ClientError:
_LOGGER.exception("ClientError")
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors={"base": "device_fail"},
step_id="user", data_schema=self.schema, errors={"base": "device_fail"},
)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error creating device")
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors={"base": "device_fail"},
step_id="user", data_schema=self.schema, errors={"base": "device_fail"},
)
mac = device.mac
return self._create_entry(host, mac, key, uuid, password)
return await self._create_entry(host, mac, key, uuid, password)
async def async_step_user(self, user_input=None):
"""User initiated config flow."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA,)
return self.async_show_form(step_id="user", data_schema=self.schema)
return await self._create_device(
user_input[CONF_HOST],
user_input.get(CONF_KEY),
@@ -111,7 +117,10 @@ class FlowHandler(config_entries.ConfigFlow):
return await self.async_step_user()
return await self._create_device(host)
async def async_step_discovery(self, user_input):
async def async_step_discovery(self, discovery_info):
"""Initialize step from discovery."""
_LOGGER.info("Discovered device: %s", user_input)
return self._create_entry(user_input[KEY_IP], user_input[KEY_MAC])
_LOGGER.debug("Discovered device: %s", discovery_info)
await self.async_set_unique_id(discovery_info[KEY_MAC])
self._abort_if_unique_id_configured()
self.host = discovery_info[KEY_IP]
return await self.async_step_user()

View File

@@ -3,7 +3,7 @@
"name": "Daikin AC",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/daikin",
"requirements": ["pydaikin==2.0.1"],
"requirements": ["pydaikin==2.0.2"],
"codeowners": ["@fredrike"],
"quality_scale": "platinum"
}

View File

@@ -3,7 +3,7 @@
"step": {
"user": {
"title": "Configure Daikin AC",
"description": "Enter IP address of your Daikin AC.",
"description": "Enter IP address of your Daikin AC.\n\nNote that [%key:common::config_flow::data::api_key%] and [%key:common::config_flow::data::password%] are used by BRP072Cxx and SKYFi devices respectively.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"key": "[%key:common::config_flow::data::api_key%]",

View File

@@ -158,23 +158,23 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Prepare configuration for a discovered forked-daapd device."""
if not (
discovery_info.get("properties")
and discovery_info["properties"].get("mtd-version")
and float(discovery_info["properties"].get("mtd-version", 0)) >= 27.0
and discovery_info["properties"].get("Machine Name")
):
return self.async_abort(reason="not_forked_daapd")
await self.async_set_unique_id(discovery_info["properties"]["Machine Name"])
self._abort_if_unique_id_configured()
# Update title and abort if we already have an entry for this host
for entry in self._async_current_entries():
if entry.data[CONF_HOST] != discovery_info["host"]:
if entry.data.get(CONF_HOST) != discovery_info["host"]:
continue
self.hass.config_entries.async_update_entry(
entry, title=discovery_info["properties"]["Machine Name"],
)
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(discovery_info["properties"]["Machine Name"])
self._abort_if_unique_id_configured()
zeroconf_data = {
CONF_HOST: discovery_info["host"],
CONF_PORT: int(discovery_info["port"]),

View File

@@ -6,7 +6,7 @@ import logging
from pyforked_daapd import ForkedDaapdAPI
from pylibrespot_java import LibrespotJavaAPI
from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC
from homeassistant.const import (
CONF_HOST,
@@ -116,7 +116,7 @@ async def update_listener(hass, entry):
)
class ForkedDaapdZone(MediaPlayerDevice):
class ForkedDaapdZone(MediaPlayerEntity):
"""Representation of a forked-daapd output."""
def __init__(self, api, output, entry_id):
@@ -221,7 +221,7 @@ class ForkedDaapdZone(MediaPlayerDevice):
return SUPPORTED_FEATURES_ZONE
class ForkedDaapdMaster(MediaPlayerDevice):
class ForkedDaapdMaster(MediaPlayerEntity):
"""Representation of the main forked-daapd device."""
def __init__(

View File

@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20200514.1"],
"requirements": ["home-assistant-frontend==20200518.0"],
"dependencies": [
"api",
"auth",

View File

@@ -75,6 +75,7 @@ from .const import (
from .util import (
convert_to_float,
dismiss_setup_message,
format_sw_version,
show_setup_message,
validate_media_player_features,
)
@@ -253,7 +254,7 @@ class HomeAccessory(Accessory):
else:
model = domain.title()
if ATTR_SOFTWARE_VERSION in self.config:
sw_version = self.config[ATTR_SOFTWARE_VERSION]
sw_version = format_sw_version(self.config[ATTR_SOFTWARE_VERSION])
else:
sw_version = __version__

View File

@@ -4,6 +4,7 @@ import io
import ipaddress
import logging
import os
import re
import secrets
import socket
@@ -415,6 +416,14 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str):
)
def format_sw_version(version):
"""Extract the version string in a format homekit can consume."""
match = re.search(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?", str(version).replace("-", "."))
if match:
return match.group(0)
return None
def migrate_filesystem_state_data_for_primary_imported_entry_id(
hass: HomeAssistant, entry_id: str
):

View File

@@ -152,6 +152,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.debug(
"Unable to determine unique id from discovery info and IPP response"
)
return self.async_abort(reason="unique_id_required")
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured(

View File

@@ -28,7 +28,8 @@
"connection_upgrade": "Failed to connect to printer due to connection upgrade being required.",
"ipp_error": "Encountered IPP error.",
"ipp_version_error": "IPP version not supported by printer.",
"parse_error": "Failed to parse response from printer."
"parse_error": "Failed to parse response from printer.",
"unique_id_required": "Device missing unique identification required for discovery."
}
}
}

View File

@@ -3,5 +3,6 @@
"name": "Mill",
"documentation": "https://www.home-assistant.io/integrations/mill",
"requirements": ["millheater==0.3.4"],
"codeowners": ["@danielhiversen"]
"codeowners": ["@danielhiversen"],
"config_flow": true
}

View File

@@ -18,7 +18,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
COORDINATOR,
@@ -61,7 +61,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_update_data():
"""Fetch data from NUT."""
async with async_timeout.timeout(10):
return await hass.async_add_executor_job(data.update)
await hass.async_add_executor_job(data.update)
if not data.status:
raise UpdateFailed("Error fetching UPS state")
coordinator = DataUpdateCoordinator(
hass,

View File

@@ -189,6 +189,8 @@ class NUTSensor(Entity):
@property
def state(self):
"""Return entity state from ups."""
if not self._data.status:
return None
if self._type == KEY_STATUS_DISPLAY:
return _format_display_state(self._data.status)
return self._data.status.get(self._type)

View File

@@ -207,8 +207,13 @@ class ONVIFDevice:
async def async_get_capabilities(self):
"""Obtain information about the available services on the device."""
media_service = self.device.create_media_service()
media_capabilities = await media_service.GetServiceCapabilities()
snapshot = False
try:
media_service = self.device.create_media_service()
media_capabilities = await media_service.GetServiceCapabilities()
snapshot = media_capabilities.SnapshotUri
except (ONVIFError, Fault):
pass
pullpoint = False
try:
@@ -225,7 +230,7 @@ class ONVIFDevice:
except ONVIFError:
pass
return Capabilities(media_capabilities.SnapshotUri, pullpoint, ptz)
return Capabilities(snapshot, pullpoint, ptz)
async def async_get_profiles(self) -> List[Profile]:
"""Obtain media profiles for this device."""

View File

@@ -5,5 +5,5 @@
"codeowners": [
"@danielhiversen"
],
"requirements": ["open-garage==0.1.3"]
"requirements": ["open-garage==0.1.4"]
}

View File

@@ -31,6 +31,7 @@ _LOGGER = logging.getLogger(__name__)
ATTR_CLEANING_TIME = "cleaning_time"
ATTR_CLEANED_AREA = "cleaned_area"
ATTR_ERROR = "error"
ATTR_ERROR_CODE = "error_code"
ATTR_POSITION = "position"
ATTR_SOFTWARE_VERSION = "software_version"
@@ -174,11 +175,6 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
# Roomba software version
software_version = state.get("softwareVer")
# Error message in plain english
error_msg = "None"
if hasattr(self.vacuum, "error_message"):
error_msg = self.vacuum.error_message
# Set properties that are to appear in the GUI
state_attrs = {ATTR_SOFTWARE_VERSION: software_version}
@@ -198,9 +194,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
state_attrs[ATTR_CLEANING_TIME] = cleaning_time
state_attrs[ATTR_CLEANED_AREA] = cleaned_area
# Skip error attr if there is none
if error_msg and error_msg != "None":
state_attrs[ATTR_ERROR] = error_msg
# Error
if self.vacuum.error_code != 0:
state_attrs[ATTR_ERROR] = self.vacuum.error_message
state_attrs[ATTR_ERROR_CODE] = self.vacuum.error_code
# Not all Roombas expose position data
# https://github.com/koalazak/dorita980/issues/48

View File

@@ -3,7 +3,7 @@
"name": "iRobot Roomba",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roomba",
"requirements": ["roombapy==1.5.3"],
"requirements": ["roombapy==1.6.1"],
"dependencies": [],
"codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"]
}

View File

@@ -14,7 +14,7 @@ from .const import (
ATTR_COMMAND,
ATTR_RATE,
DOMAIN,
EVENT_UPB_LINK_CHANGED,
EVENT_UPB_SCENE_CHANGED,
)
UPB_PLATFORMS = ["light", "scene"]
@@ -49,7 +49,7 @@ async def async_setup_entry(hass, config_entry):
return
hass.bus.async_fire(
EVENT_UPB_LINK_CHANGED,
EVENT_UPB_SCENE_CHANGED,
{
ATTR_COMMAND: change["command"],
ATTR_ADDRESS: element.addr.index,

View File

@@ -13,7 +13,7 @@ ATTR_BRIGHTNESS_PCT = "brightness_pct"
ATTR_COMMAND = "command"
ATTR_RATE = "rate"
CONF_NETWORK = "network"
EVENT_UPB_LINK_CHANGED = "upb.link_changed"
EVENT_UPB_SCENE_CHANGED = "upb.scene_changed"
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100))

View File

@@ -9,7 +9,7 @@
"zha-quirks==0.0.39",
"zigpy-cc==0.4.2",
"zigpy-deconz==0.9.2",
"zigpy==0.20.3",
"zigpy==0.20.4",
"zigpy-xbee==0.12.1",
"zigpy-zigate==0.6.1"
],

View File

@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 110
PATCH_VERSION = "0b2"
PATCH_VERSION = "0b4"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 0)

View File

@@ -1454,10 +1454,6 @@ class Config:
)
data = await store.async_load()
if data and "external_url" in data:
self._update(source=SOURCE_STORAGE, **data)
return
async def migrate_base_url(_: Event) -> None:
"""Migrate base_url to internal_url/external_url."""
if self.hass.config.api is None:
@@ -1484,11 +1480,24 @@ class Config:
external_url=network.normalize_url(str(base_url))
)
# Try to migrate base_url to internal_url/external_url
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, migrate_base_url)
if data:
self._update(source=SOURCE_STORAGE, **data)
# Try to migrate base_url to internal_url/external_url
if "external_url" not in data:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, migrate_base_url
)
self._update(
source=SOURCE_STORAGE,
latitude=data.get("latitude"),
longitude=data.get("longitude"),
elevation=data.get("elevation"),
unit_system=data.get("unit_system"),
location_name=data.get("location_name"),
time_zone=data.get("time_zone"),
external_url=data.get("external_url", _UNDEF),
internal_url=data.get("internal_url", _UNDEF),
)
async def async_store(self) -> None:
"""Store [homeassistant] core config."""

View File

@@ -89,6 +89,7 @@ FLOWS = [
"met",
"meteo_france",
"mikrotik",
"mill",
"minecraft_server",
"mobile_app",
"monoprice",

View File

@@ -12,7 +12,7 @@ cryptography==2.9.2
defusedxml==0.6.0
distro==1.5.0
hass-nabucasa==0.34.2
home-assistant-frontend==20200514.1
home-assistant-frontend==20200518.0
importlib-metadata==1.6.0
jinja2>=2.11.1
netdisco==2.6.0

View File

@@ -731,7 +731,7 @@ hole==0.5.1
holidays==0.10.2
# homeassistant.components.frontend
home-assistant-frontend==20200514.1
home-assistant-frontend==20200518.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -997,7 +997,7 @@ onkyo-eiscp==1.2.7
onvif-zeep-async==0.3.0
# homeassistant.components.opengarage
open-garage==0.1.3
open-garage==0.1.4
# homeassistant.components.opencv
# opencv-python-headless==4.2.0.32
@@ -1212,7 +1212,7 @@ pyalmond==0.0.2
pyarlo==0.2.3
# homeassistant.components.atag
pyatag==0.3.1.1
pyatag==0.3.1.2
# homeassistant.components.netatmo
pyatmo==3.3.1
@@ -1263,7 +1263,7 @@ pycsspeechtts==1.0.3
# pycups==1.9.73
# homeassistant.components.daikin
pydaikin==2.0.1
pydaikin==2.0.2
# homeassistant.components.danfoss_air
pydanfossair==0.1.0
@@ -1874,7 +1874,7 @@ rocketchat-API==0.6.1
rokuecp==0.4.0
# homeassistant.components.roomba
roombapy==1.5.3
roombapy==1.6.1
# homeassistant.components.rova
rova==0.1.0
@@ -2263,7 +2263,7 @@ zigpy-xbee==0.12.1
zigpy-zigate==0.6.1
# homeassistant.components.zha
zigpy==0.20.3
zigpy==0.20.4
# homeassistant.components.zoneminder
zm-py==0.4.0

View File

@@ -312,7 +312,7 @@ hole==0.5.1
holidays==0.10.2
# homeassistant.components.frontend
home-assistant-frontend==20200514.1
home-assistant-frontend==20200518.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -515,7 +515,7 @@ pyalmond==0.0.2
pyarlo==0.2.3
# homeassistant.components.atag
pyatag==0.3.1.1
pyatag==0.3.1.2
# homeassistant.components.netatmo
pyatmo==3.3.1
@@ -533,7 +533,7 @@ pychromecast==5.1.0
pycoolmasternet==0.0.4
# homeassistant.components.daikin
pydaikin==2.0.1
pydaikin==2.0.2
# homeassistant.components.deconz
pydeconz==70
@@ -765,7 +765,7 @@ ring_doorbell==0.6.0
rokuecp==0.4.0
# homeassistant.components.roomba
roombapy==1.5.3
roombapy==1.6.1
# homeassistant.components.yamaha
rxv==0.6.0
@@ -918,4 +918,4 @@ zigpy-xbee==0.12.1
zigpy-zigate==0.6.1
# homeassistant.components.zha
zigpy==0.20.3
zigpy==0.20.4

View File

@@ -6,8 +6,8 @@ from aiohttp import ClientError
from aiohttp.web_exceptions import HTTPForbidden
import pytest
from homeassistant.components.daikin import config_flow
from homeassistant.components.daikin.const import KEY_IP, KEY_MAC
from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_HOST
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
@@ -22,13 +22,6 @@ MAC = "AABBCCDDEEFF"
HOST = "127.0.0.1"
def init_config_flow(hass):
"""Init a configuration flow."""
flow = config_flow.FlowHandler()
flow.hass = hass
return flow
@pytest.fixture
def mock_daikin():
"""Mock pydaikin."""
@@ -45,13 +38,16 @@ def mock_daikin():
async def test_user(hass, mock_daikin):
"""Test user config."""
flow = init_config_flow(hass)
result = await hass.config_entries.flow.async_init(
"daikin", context={"source": SOURCE_USER},
)
result = await flow.async_step_user()
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await flow.async_step_user({CONF_HOST: HOST})
result = await hass.config_entries.flow.async_init(
"daikin", context={"source": SOURCE_USER}, data={CONF_HOST: HOST},
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == HOST
assert result["data"][CONF_HOST] == HOST
@@ -60,34 +56,26 @@ async def test_user(hass, mock_daikin):
async def test_abort_if_already_setup(hass, mock_daikin):
"""Test we abort if Daikin is already setup."""
flow = init_config_flow(hass)
MockConfigEntry(domain="daikin", data={KEY_MAC: MAC}).add_to_hass(hass)
MockConfigEntry(domain="daikin", unique_id=MAC).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
"daikin", context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC},
)
result = await flow.async_step_user({CONF_HOST: HOST})
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_import(hass, mock_daikin):
"""Test import step."""
flow = init_config_flow(hass)
result = await flow.async_step_import({})
result = await hass.config_entries.flow.async_init(
"daikin", context={"source": SOURCE_IMPORT}, data={},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await flow.async_step_import({CONF_HOST: HOST})
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == HOST
assert result["data"][CONF_HOST] == HOST
assert result["data"][KEY_MAC] == MAC
async def test_discovery(hass, mock_daikin):
"""Test discovery step."""
flow = init_config_flow(hass)
result = await flow.async_step_discovery({KEY_IP: HOST, KEY_MAC: MAC})
result = await hass.config_entries.flow.async_init(
"daikin", context={"source": SOURCE_IMPORT}, data={CONF_HOST: HOST},
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == HOST
assert result["data"][CONF_HOST] == HOST
@@ -105,10 +93,31 @@ async def test_discovery(hass, mock_daikin):
)
async def test_device_abort(hass, mock_daikin, s_effect, reason):
"""Test device abort."""
flow = init_config_flow(hass)
mock_daikin.factory.side_effect = s_effect
result = await flow.async_step_user({CONF_HOST: HOST})
result = await hass.config_entries.flow.async_init(
"daikin", context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": reason}
assert result["step_id"] == "user"
@pytest.mark.parametrize(
"source, data, unique_id", [(SOURCE_DISCOVERY, {KEY_IP: HOST, KEY_MAC: MAC}, MAC)],
)
async def test_discovery(hass, mock_daikin, source, data, unique_id):
"""Test discovery/zeroconf step."""
result = await hass.config_entries.flow.async_init(
"daikin", context={"source": source}, data=data,
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
MockConfigEntry(domain="daikin", unique_id=unique_id).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
"daikin", context={"source": source}, data=data,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_in_progress"

View File

@@ -103,7 +103,7 @@ async def test_zeroconf_updates_title(hass, config_entry):
discovery_info = {
"host": "192.168.1.1",
"port": 23,
"properties": {"mtd-version": 1, "Machine Name": "zeroconf_test"},
"properties": {"mtd-version": 27.0, "Machine Name": "zeroconf_test"},
}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info
@@ -143,7 +143,7 @@ async def test_config_flow_zeroconf_valid(hass):
"host": "192.168.1.1",
"port": 23,
"properties": {
"mtd-version": 1,
"mtd-version": 27.0,
"Machine Name": "zeroconf_test",
"Machine ID": "5E55EEFF",
},

View File

@@ -28,6 +28,7 @@ from homeassistant.components.homekit.util import (
density_to_air_quality,
dismiss_setup_message,
find_next_available_port,
format_sw_version,
port_is_available,
show_setup_message,
temperature_to_homekit,
@@ -315,3 +316,12 @@ async def test_port_is_available(hass):
assert next_port
assert await hass.async_add_executor_job(port_is_available, next_port)
async def test_format_sw_version():
"""Test format_sw_version method."""
assert format_sw_version("soho+3.6.8+soho-release-rt120+10") == "3.6.8"
assert format_sw_version("undefined-undefined-1.6.8") == "1.6.8"
assert format_sw_version("56.0-76060") == "56.0.76060"
assert format_sw_version(3.6) == "3.6"
assert format_sw_version("unknown") is None

View File

@@ -1,6 +1,9 @@
"""Tests for the IPP integration."""
import os
import aiohttp
from pyipp import IPPConnectionUpgradeRequired, IPPError
from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN
from homeassistant.const import (
CONF_HOST,
@@ -18,21 +21,25 @@ from tests.test_util.aiohttp import AiohttpClientMocker
ATTR_HOSTNAME = "hostname"
ATTR_PROPERTIES = "properties"
HOST = "192.168.1.31"
PORT = 631
BASE_PATH = "/ipp/print"
IPP_ZEROCONF_SERVICE_TYPE = "_ipp._tcp.local."
IPPS_ZEROCONF_SERVICE_TYPE = "_ipps._tcp.local."
ZEROCONF_NAME = "EPSON XP-6000 Series"
ZEROCONF_HOST = "192.168.1.31"
ZEROCONF_HOST = HOST
ZEROCONF_HOSTNAME = "EPSON123456.local."
ZEROCONF_PORT = 631
ZEROCONF_PORT = PORT
ZEROCONF_RP = "ipp/print"
MOCK_USER_INPUT = {
CONF_HOST: "192.168.1.31",
CONF_PORT: 361,
CONF_HOST: HOST,
CONF_PORT: PORT,
CONF_SSL: False,
CONF_VERIFY_SSL: False,
CONF_BASE_PATH: "/ipp/print",
CONF_BASE_PATH: BASE_PATH,
}
MOCK_ZEROCONF_IPP_SERVICE_INFO = {
@@ -41,7 +48,7 @@ MOCK_ZEROCONF_IPP_SERVICE_INFO = {
CONF_HOST: ZEROCONF_HOST,
ATTR_HOSTNAME: ZEROCONF_HOSTNAME,
CONF_PORT: ZEROCONF_PORT,
ATTR_PROPERTIES: {"rp": "ipp/print"},
ATTR_PROPERTIES: {"rp": ZEROCONF_RP},
}
MOCK_ZEROCONF_IPPS_SERVICE_INFO = {
@@ -50,7 +57,7 @@ MOCK_ZEROCONF_IPPS_SERVICE_INFO = {
CONF_HOST: ZEROCONF_HOST,
ATTR_HOSTNAME: ZEROCONF_HOSTNAME,
CONF_PORT: ZEROCONF_PORT,
ATTR_PROPERTIES: {"rp": "ipp/print"},
ATTR_PROPERTIES: {"rp": ZEROCONF_RP},
}
@@ -61,30 +68,75 @@ def load_fixture_binary(filename):
return fptr.read()
def mock_connection(
aioclient_mock: AiohttpClientMocker,
host: str = HOST,
port: int = PORT,
ssl: bool = False,
base_path: str = BASE_PATH,
conn_error: bool = False,
conn_upgrade_error: bool = False,
ipp_error: bool = False,
no_unique_id: bool = False,
parse_error: bool = False,
version_not_supported: bool = False,
):
"""Mock the IPP connection."""
scheme = "https" if ssl else "http"
ipp_url = f"{scheme}://{host}:{port}"
if ipp_error:
aioclient_mock.post(f"{ipp_url}{base_path}", exc=IPPError)
return
if conn_error:
aioclient_mock.post(f"{ipp_url}{base_path}", exc=aiohttp.ClientError)
return
if conn_upgrade_error:
aioclient_mock.post(f"{ipp_url}{base_path}", exc=IPPConnectionUpgradeRequired)
return
fixture = "ipp/get-printer-attributes.bin"
if no_unique_id:
fixture = "ipp/get-printer-attributes-success-nodata.bin"
elif version_not_supported:
fixture = "ipp/get-printer-attributes-error-0x0503.bin"
if parse_error:
content = "BAD"
else:
content = load_fixture_binary(fixture)
aioclient_mock.post(
f"{ipp_url}{base_path}",
content=content,
headers={"Content-Type": "application/ipp"},
)
async def init_integration(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
skip_setup: bool = False,
host: str = HOST,
port: int = PORT,
ssl: bool = False,
base_path: str = BASE_PATH,
uuid: str = "cfe92100-67c4-11d4-a45f-f8d027761251",
unique_id: str = "cfe92100-67c4-11d4-a45f-f8d027761251",
conn_error: bool = False,
) -> MockConfigEntry:
"""Set up the IPP integration in Home Assistant."""
fixture = "ipp/get-printer-attributes.bin"
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary(fixture),
headers={"Content-Type": "application/ipp"},
)
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=unique_id,
data={
CONF_HOST: "192.168.1.31",
CONF_PORT: 631,
CONF_SSL: False,
CONF_HOST: host,
CONF_PORT: port,
CONF_SSL: ssl,
CONF_VERIFY_SSL: True,
CONF_BASE_PATH: "/ipp/print",
CONF_BASE_PATH: base_path,
CONF_UUID: uuid,
},
)
@@ -92,6 +144,14 @@ async def init_integration(
entry.add_to_hass(hass)
if not skip_setup:
mock_connection(
aioclient_mock,
host=host,
port=port,
ssl=ssl,
base_path=base_path,
conn_error=conn_error,
)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

View File

@@ -1,7 +1,4 @@
"""Tests for the IPP config flow."""
import aiohttp
from pyipp import IPPConnectionUpgradeRequired, IPPError
from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL
@@ -17,7 +14,7 @@ from . import (
MOCK_ZEROCONF_IPP_SERVICE_INFO,
MOCK_ZEROCONF_IPPS_SERVICE_INFO,
init_integration,
load_fixture_binary,
mock_connection,
)
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -37,11 +34,7 @@ async def test_show_zeroconf_form(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test that the zeroconf confirmation form is served."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
headers={"Content-Type": "application/ipp"},
)
mock_connection(aioclient_mock)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
@@ -57,7 +50,7 @@ async def test_connection_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we show user form on IPP connection error."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError)
mock_connection(aioclient_mock, conn_error=True)
user_input = MOCK_USER_INPUT.copy()
result = await hass.config_entries.flow.async_init(
@@ -73,7 +66,7 @@ async def test_zeroconf_connection_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP connection error."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError)
mock_connection(aioclient_mock, conn_error=True)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
@@ -88,7 +81,7 @@ async def test_zeroconf_confirm_connection_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP connection error."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError)
mock_connection(aioclient_mock, conn_error=True)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
@@ -103,9 +96,7 @@ async def test_user_connection_upgrade_required(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we show the user form if connection upgrade required by server."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print", exc=IPPConnectionUpgradeRequired
)
mock_connection(aioclient_mock, conn_upgrade_error=True)
user_input = MOCK_USER_INPUT.copy()
result = await hass.config_entries.flow.async_init(
@@ -121,9 +112,7 @@ async def test_zeroconf_connection_upgrade_required(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP connection error."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print", exc=IPPConnectionUpgradeRequired
)
mock_connection(aioclient_mock, conn_upgrade_error=True)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
@@ -138,11 +127,7 @@ async def test_user_parse_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort user flow on IPP parse error."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content="BAD",
headers={"Content-Type": "application/ipp"},
)
mock_connection(aioclient_mock, parse_error=True)
user_input = MOCK_USER_INPUT.copy()
result = await hass.config_entries.flow.async_init(
@@ -157,11 +142,7 @@ async def test_zeroconf_parse_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP parse error."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content="BAD",
headers={"Content-Type": "application/ipp"},
)
mock_connection(aioclient_mock, parse_error=True)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
@@ -176,7 +157,7 @@ async def test_user_ipp_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort the user flow on IPP error."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError)
mock_connection(aioclient_mock, ipp_error=True)
user_input = MOCK_USER_INPUT.copy()
result = await hass.config_entries.flow.async_init(
@@ -191,7 +172,7 @@ async def test_zeroconf_ipp_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP error."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError)
mock_connection(aioclient_mock, ipp_error=True)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
@@ -206,11 +187,7 @@ async def test_user_ipp_version_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort user flow on IPP version not supported error."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"),
headers={"Content-Type": "application/ipp"},
)
mock_connection(aioclient_mock, version_not_supported=True)
user_input = {**MOCK_USER_INPUT}
result = await hass.config_entries.flow.async_init(
@@ -225,11 +202,7 @@ async def test_zeroconf_ipp_version_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP version not supported error."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"),
headers={"Content-Type": "application/ipp"},
)
mock_connection(aioclient_mock, version_not_supported=True)
discovery_info = {**MOCK_ZEROCONF_IPP_SERVICE_INFO}
result = await hass.config_entries.flow.async_init(
@@ -291,15 +264,26 @@ async def test_zeroconf_with_uuid_device_exists_abort(
assert result["reason"] == "already_configured"
async def test_zeroconf_unique_id_required_abort(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow if printer lacks unique identification."""
mock_connection(aioclient_mock, no_unique_id=True)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unique_id_required"
async def test_full_user_flow_implementation(
hass: HomeAssistant, aioclient_mock
) -> None:
"""Test the full manual user flow from start to finish."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
headers={"Content-Type": "application/ipp"},
)
mock_connection(aioclient_mock)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER},
@@ -328,11 +312,7 @@ async def test_full_zeroconf_flow_implementation(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the full manual user flow from start to finish."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
headers={"Content-Type": "application/ipp"},
)
mock_connection(aioclient_mock)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
@@ -363,11 +343,7 @@ async def test_full_zeroconf_tls_flow_implementation(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the full manual user flow from start to finish."""
aioclient_mock.post(
"https://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
headers={"Content-Type": "application/ipp"},
)
mock_connection(aioclient_mock, ssl=True)
discovery_info = MOCK_ZEROCONF_IPPS_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(

View File

@@ -1,6 +1,4 @@
"""Tests for the IPP integration."""
import aiohttp
from homeassistant.components.ipp.const import DOMAIN
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
@@ -17,9 +15,7 @@ async def test_config_entry_not_ready(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the IPP configuration entry not ready."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError)
entry = await init_integration(hass, aioclient_mock)
entry = await init_integration(hass, aioclient_mock, conn_error=True)
assert entry.state == ENTRY_STATE_SETUP_RETRY

View File

@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from tests.async_mock import patch
from tests.components.ipp import init_integration
from tests.components.ipp import init_integration, mock_connection
from tests.test_util.aiohttp import AiohttpClientMocker
@@ -16,6 +16,8 @@ async def test_sensors(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the creation and values of the IPP sensors."""
mock_connection(aioclient_mock)
entry = await init_integration(hass, aioclient_mock, skip_setup=True)
registry = await hass.helpers.entity_registry.async_get_registry()

Binary file not shown.

View File

@@ -1294,17 +1294,17 @@ async def test_migration_base_url(hass, hass_storage):
with patch.object(hass.bus, "async_listen_once") as mock_listen:
# Empty config
await config.async_load()
assert len(mock_listen.mock_calls) == 1
assert len(mock_listen.mock_calls) == 0
# With just a name
stored["data"] = {"location_name": "Test Name"}
await config.async_load()
assert len(mock_listen.mock_calls) == 2
assert len(mock_listen.mock_calls) == 1
# With external url
stored["data"]["external_url"] = "https://example.com"
await config.async_load()
assert len(mock_listen.mock_calls) == 2
assert len(mock_listen.mock_calls) == 1
# Test that the event listener works
assert mock_listen.mock_calls[0][1][0] == EVENT_HOMEASSISTANT_START
@@ -1319,3 +1319,14 @@ async def test_migration_base_url(hass, hass_storage):
hass.config.api = Mock(deprecated_base_url=internal)
await mock_listen.mock_calls[0][1][1](None)
assert config.internal_url == internal
async def test_additional_data_in_core_config(hass, hass_storage):
"""Test that we can handle additional data in core configuration."""
config = ha.Config(hass)
hass_storage[ha.CORE_STORAGE_KEY] = {
"version": 1,
"data": {"location_name": "Test Name", "additional_valid_key": "value"},
}
await config.async_load()
assert config.location_name == "Test Name"