Change Plugwise integration to plugwise module (#43036)

* Switch to plugwise module and forthcoming changes

* Adjusted according to review

* Fix leaving out domain for tests

* Add tests for exceptions

* Add more tests for exceptions

* Version bump

* Wording on test

* Catch-up with dev
This commit is contained in:
Tom 2020-11-21 03:43:20 +01:00 committed by GitHub
parent e32669a2d9
commit db60a71603
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 163 additions and 86 deletions

View File

@ -336,7 +336,7 @@ homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn
homeassistant/components/pilight/* @trekky12
homeassistant/components/plaato/* @JohNan
homeassistant/components/plex/* @jjlawren
homeassistant/components/plugwise/* @CoMPaTech @bouwew
homeassistant/components/plugwise/* @CoMPaTech @bouwew @brefra
homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa
homeassistant/components/point/* @fredrike
homeassistant/components/poolsense/* @haemishkyd

View File

@ -1,16 +1,10 @@
"""Plugwise platform for Home Assistant Core."""
import asyncio
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from .const import ALL_PLATFORMS, DOMAIN, UNDO_UPDATE_LISTENER
from .gateway import async_setup_entry_gw
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
from .gateway import async_setup_entry_gw, async_unload_entry_gw
async def async_setup(hass: HomeAssistant, config: dict):
@ -27,19 +21,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in ALL_PLATFORMS
]
)
)
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
"""Unload the Plugwise components."""
if entry.data.get(CONF_HOST):
return await async_unload_entry_gw(hass, entry)
# PLACEHOLDER USB entry setup
return False

View File

@ -2,7 +2,7 @@
import logging
from Plugwise_Smile.Smile import Smile
from plugwise.exceptions import PlugwiseException
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@ -192,7 +192,7 @@ class PwThermostat(SmileGateway, ClimateEntity):
await self._api.set_temperature(self._loc_id, temperature)
self._setpoint = temperature
self.async_write_ha_state()
except Smile.PlugwiseError:
except PlugwiseException:
_LOGGER.error("Error while communicating to device")
else:
_LOGGER.error("Invalid temperature requested")
@ -205,7 +205,7 @@ class PwThermostat(SmileGateway, ClimateEntity):
try:
await self._api.set_temperature(self._loc_id, self._schedule_temp)
self._setpoint = self._schedule_temp
except Smile.PlugwiseError:
except PlugwiseException:
_LOGGER.error("Error while communicating to device")
try:
await self._api.set_schedule_state(
@ -213,7 +213,7 @@ class PwThermostat(SmileGateway, ClimateEntity):
)
self._hvac_mode = hvac_mode
self.async_write_ha_state()
except Smile.PlugwiseError:
except PlugwiseException:
_LOGGER.error("Error while communicating to device")
async def async_set_preset_mode(self, preset_mode):
@ -223,7 +223,7 @@ class PwThermostat(SmileGateway, ClimateEntity):
self._preset_mode = preset_mode
self._setpoint = self._presets.get(self._preset_mode, "none")[0]
self.async_write_ha_state()
except Smile.PlugwiseError:
except PlugwiseException:
_LOGGER.error("Error while communicating to device")
@callback

View File

@ -1,7 +1,8 @@
"""Config flow for Plugwise integration."""
import logging
from Plugwise_Smile.Smile import Smile
from plugwise.exceptions import InvalidAuthentication, PlugwiseException
from plugwise.smile import Smile
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
@ -67,9 +68,9 @@ async def validate_gw_input(hass: core.HomeAssistant, data):
try:
await api.connect()
except Smile.InvalidAuthentication as err:
except InvalidAuthentication as err:
raise InvalidAuth from err
except Smile.PlugwiseError as err:
except PlugwiseException as err:
raise CannotConnect from err
return api

View File

@ -2,7 +2,9 @@
DOMAIN = "plugwise"
SENSOR_PLATFORMS = ["sensor", "switch"]
ALL_PLATFORMS = ["binary_sensor", "climate", "sensor", "switch"]
PLATFORMS_GATEWAY = ["binary_sensor", "climate", "sensor", "switch"]
PW_TYPE = "plugwise_type"
GATEWAY = "gateway"
# Sensor mapping
SENSOR_MAP_DEVICE_CLASS = 2

View File

@ -5,8 +5,13 @@ from datetime import timedelta
import logging
from typing import Dict
from Plugwise_Smile.Smile import Smile
import async_timeout
from plugwise.exceptions import (
InvalidAuthentication,
PlugwiseException,
XMLDataMissingError,
)
from plugwise.smile import Smile
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
@ -28,13 +33,15 @@ from homeassistant.helpers.update_coordinator import (
)
from .const import (
ALL_PLATFORMS,
COORDINATOR,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
DEFAULT_TIMEOUT,
DEFAULT_USERNAME,
DOMAIN,
GATEWAY,
PLATFORMS_GATEWAY,
PW_TYPE,
SENSOR_PLATFORMS,
UNDO_UPDATE_LISTENER,
)
@ -64,11 +71,11 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("Unable to connect to Smile")
raise ConfigEntryNotReady
except Smile.InvalidAuthentication:
except InvalidAuthentication:
_LOGGER.error("Invalid username or Smile ID")
return False
except Smile.PlugwiseError as err:
except PlugwiseException as err:
_LOGGER.error("Error while communicating to device %s", api.smile_name)
raise ConfigEntryNotReady from err
@ -88,7 +95,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async with async_timeout.timeout(DEFAULT_TIMEOUT):
await api.full_update_device()
return True
except Smile.XMLDataMissingError as err:
except XMLDataMissingError as err:
raise UpdateFailed("Smile update failed") from err
coordinator = DataUpdateCoordinator(
@ -115,6 +122,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
"api": api,
COORDINATOR: coordinator,
PW_TYPE: GATEWAY,
UNDO_UPDATE_LISTENER: undo_listener,
}
@ -130,7 +138,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
single_master_thermostat = api.single_master_thermostat()
platforms = ALL_PLATFORMS
platforms = PLATFORMS_GATEWAY
if single_master_thermostat is None:
platforms = SENSOR_PLATFORMS
@ -150,13 +158,13 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry):
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in ALL_PLATFORMS
for component in PLATFORMS_GATEWAY
]
)
)

View File

@ -2,8 +2,8 @@
"domain": "plugwise",
"name": "Plugwise",
"documentation": "https://www.home-assistant.io/integrations/plugwise",
"requirements": ["Plugwise_Smile==1.6.0"],
"codeowners": ["@CoMPaTech", "@bouwew"],
"requirements": ["plugwise==0.8.3"],
"codeowners": ["@CoMPaTech", "@bouwew", "@brefra"],
"zeroconf": ["_plugwise._tcp.local."],
"config_flow": true
}

View File

@ -2,7 +2,7 @@
import logging
from Plugwise_Smile.Smile import Smile
from plugwise.exceptions import PlugwiseException
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
@ -14,6 +14,12 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Smile switches from a config entry."""
# PLACEHOLDER USB entry setup
return await async_setup_entry_gateway(hass, config_entry, async_add_entities)
async def async_setup_entry_gateway(hass, config_entry, async_add_entities):
"""Set up the Smile switches from a config entry."""
api = hass.data[DOMAIN][config_entry.entry_id]["api"]
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
@ -37,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
model = "Switch Group"
entities.append(
PwSwitch(
GwSwitch(
api, coordinator, device_properties["name"], dev_id, members, model
)
)
@ -45,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
class PwSwitch(SmileGateway, SwitchEntity):
class GwSwitch(SmileGateway, SwitchEntity):
"""Representation of a Plugwise plug."""
def __init__(self, api, coordinator, name, dev_id, members, model):
@ -79,7 +85,7 @@ class PwSwitch(SmileGateway, SwitchEntity):
if state_on:
self._is_on = True
self.async_write_ha_state()
except Smile.PlugwiseError:
except PlugwiseException:
_LOGGER.error("Error while communicating to device")
async def async_turn_off(self, **kwargs):
@ -91,7 +97,7 @@ class PwSwitch(SmileGateway, SwitchEntity):
if state_off:
self._is_on = False
self.async_write_ha_state()
except Smile.PlugwiseError:
except PlugwiseException:
_LOGGER.error("Error while communicating to device")
@callback

View File

@ -25,9 +25,6 @@ Mastodon.py==1.5.1
# homeassistant.components.orangepi_gpio
OPi.GPIO==0.4.0
# homeassistant.components.plugwise
Plugwise_Smile==1.6.0
# homeassistant.components.essent
PyEssent==0.14
@ -1139,6 +1136,9 @@ plexauth==0.0.6
# homeassistant.components.plex
plexwebsocket==0.0.12
# homeassistant.components.plugwise
plugwise==0.8.3
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11

View File

@ -6,9 +6,6 @@
# homeassistant.components.homekit
HAP-python==3.0.0
# homeassistant.components.plugwise
Plugwise_Smile==1.6.0
# homeassistant.components.flick_electric
PyFlick==0.0.2
@ -562,6 +559,9 @@ plexauth==0.0.6
# homeassistant.components.plex
plexwebsocket==0.0.12
# homeassistant.components.plugwise
plugwise==0.8.3
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11

View File

@ -1,6 +1,6 @@
"""Common initialisation for the Plugwise integration."""
from homeassistant.components.plugwise import DOMAIN
from homeassistant.components.plugwise.const import DOMAIN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry

View File

@ -3,8 +3,13 @@
from functools import partial
import re
from Plugwise_Smile.Smile import Smile
import jsonpickle
from plugwise.exceptions import (
ConnectionFailedError,
InvalidAuthentication,
PlugwiseException,
XMLDataMissingError,
)
import pytest
from tests.async_mock import AsyncMock, Mock, patch
@ -24,8 +29,8 @@ def mock_smile():
with patch(
"homeassistant.components.plugwise.config_flow.Smile",
) as smile_mock:
smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
smile_mock.InvalidAuthentication = InvalidAuthentication
smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.return_value.connect.return_value = True
yield smile_mock.return_value
@ -48,9 +53,9 @@ def mock_smile_error(aioclient_mock: AiohttpClientMocker) -> None:
def mock_smile_notconnect():
"""Mock the Plugwise Smile general connection failure for Home Assistant."""
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
smile_mock.PlugwiseError = Smile.PlugwiseError
smile_mock.InvalidAuthentication = InvalidAuthentication
smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.PlugwiseException = PlugwiseException
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=False)
yield smile_mock.return_value
@ -65,9 +70,9 @@ def mock_smile_adam():
"""Create a Mock Adam environment for testing exceptions."""
chosen_env = "adam_multiple_devices_per_zone"
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
smile_mock.XMLDataMissingError = Smile.XMLDataMissingError
smile_mock.InvalidAuthentication = InvalidAuthentication
smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.XMLDataMissingError = XMLDataMissingError
smile_mock.return_value.gateway_id = "fe799307f1624099878210aa0b9f1475"
smile_mock.return_value.heater_id = "90986d591dcd426cae3ec3e8111ff730"
@ -110,9 +115,9 @@ def mock_smile_anna():
"""Create a Mock Anna environment for testing exceptions."""
chosen_env = "anna_heatpump"
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
smile_mock.XMLDataMissingError = Smile.XMLDataMissingError
smile_mock.InvalidAuthentication = InvalidAuthentication
smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.XMLDataMissingError = XMLDataMissingError
smile_mock.return_value.gateway_id = "015ae9ea3f964e668e490fa39da3870b"
smile_mock.return_value.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927"
@ -155,9 +160,9 @@ def mock_smile_p1():
"""Create a Mock P1 DSMR environment for testing exceptions."""
chosen_env = "p1v3_full_option"
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
smile_mock.XMLDataMissingError = Smile.XMLDataMissingError
smile_mock.InvalidAuthentication = InvalidAuthentication
smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.XMLDataMissingError = XMLDataMissingError
smile_mock.return_value.gateway_id = "e950c7d5e1ee407a858e2a8b5016c8b3"
smile_mock.return_value.heater_id = None
@ -191,9 +196,9 @@ def mock_stretch():
"""Create a Mock Stretch environment for testing exceptions."""
chosen_env = "stretch_v31"
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
smile_mock.XMLDataMissingError = Smile.XMLDataMissingError
smile_mock.InvalidAuthentication = InvalidAuthentication
smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.XMLDataMissingError = XMLDataMissingError
smile_mock.return_value.gateway_id = "259882df3c05415b99c2d962534ce820"
smile_mock.return_value.heater_id = None

View File

@ -1,5 +1,8 @@
"""Tests for the Plugwise Climate integration."""
from plugwise.exceptions import PlugwiseException
from homeassistant.components.climate.const import HVAC_MODE_AUTO, HVAC_MODE_HEAT
from homeassistant.config_entries import ENTRY_STATE_LOADED
from tests.components.plugwise.common import async_init_integration
@ -13,7 +16,7 @@ async def test_adam_climate_entity_attributes(hass, mock_smile_adam):
state = hass.states.get("climate.zone_lisa_wk")
attrs = state.attributes
assert attrs["hvac_modes"] == ["heat", "auto"]
assert attrs["hvac_modes"] == [HVAC_MODE_HEAT, HVAC_MODE_AUTO]
assert "preset_modes" in attrs
assert "no_frost" in attrs["preset_modes"]
@ -29,7 +32,7 @@ async def test_adam_climate_entity_attributes(hass, mock_smile_adam):
state = hass.states.get("climate.zone_thermostat_jessie")
attrs = state.attributes
assert attrs["hvac_modes"] == ["heat", "auto"]
assert attrs["hvac_modes"] == [HVAC_MODE_HEAT, HVAC_MODE_AUTO]
assert "preset_modes" in attrs
assert "no_frost" in attrs["preset_modes"]
@ -41,6 +44,44 @@ async def test_adam_climate_entity_attributes(hass, mock_smile_adam):
assert attrs["preset_mode"] == "asleep"
async def test_adam_climate_adjust_negative_testing(hass, mock_smile_adam):
"""Test exceptions of climate entities."""
mock_smile_adam.set_preset.side_effect = PlugwiseException
mock_smile_adam.set_schedule_state.side_effect = PlugwiseException
mock_smile_adam.set_temperature.side_effect = PlugwiseException
entry = await async_init_integration(hass, mock_smile_adam)
assert entry.state == ENTRY_STATE_LOADED
await hass.services.async_call(
"climate",
"set_temperature",
{"entity_id": "climate.zone_lisa_wk", "temperature": 25},
blocking=True,
)
state = hass.states.get("climate.zone_lisa_wk")
attrs = state.attributes
assert attrs["temperature"] == 21.5
await hass.services.async_call(
"climate",
"set_preset_mode",
{"entity_id": "climate.zone_thermostat_jessie", "preset_mode": "home"},
blocking=True,
)
state = hass.states.get("climate.zone_thermostat_jessie")
attrs = state.attributes
assert attrs["preset_mode"] == "asleep"
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": "climate.zone_thermostat_jessie", "hvac_mode": HVAC_MODE_AUTO},
blocking=True,
)
state = hass.states.get("climate.zone_thermostat_jessie")
attrs = state.attributes
async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam):
"""Test handling of user requests in adam climate device environment."""
entry = await async_init_integration(hass, mock_smile_adam)
@ -112,7 +153,7 @@ async def test_anna_climate_entity_attributes(hass, mock_smile_anna):
assert attrs["current_temperature"] == 23.3
assert attrs["temperature"] == 21.0
assert state.state == "auto"
assert state.state == HVAC_MODE_AUTO
assert attrs["hvac_action"] == "idle"
assert attrs["preset_mode"] == "home"

View File

@ -1,5 +1,9 @@
"""Test the Plugwise config flow."""
from Plugwise_Smile.Smile import Smile
from plugwise.exceptions import (
ConnectionFailedError,
InvalidAuthentication,
PlugwiseException,
)
import pytest
from homeassistant import config_entries, data_entry_flow, setup
@ -47,9 +51,9 @@ def mock_smile():
with patch(
"homeassistant.components.plugwise.config_flow.Smile",
) as smile_mock:
smile_mock.PlugwiseError = Smile.PlugwiseError
smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
smile_mock.PlugwiseError = PlugwiseException
smile_mock.InvalidAuthentication = InvalidAuthentication
smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.return_value.connect.return_value = True
yield smile_mock.return_value
@ -207,7 +211,7 @@ async def test_form_invalid_auth(hass, mock_smile):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mock_smile.connect.side_effect = Smile.InvalidAuthentication
mock_smile.connect.side_effect = InvalidAuthentication
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
result2 = await hass.config_entries.flow.async_configure(
@ -225,7 +229,7 @@ async def test_form_cannot_connect(hass, mock_smile):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mock_smile.connect.side_effect = Smile.ConnectionFailedError
mock_smile.connect.side_effect = ConnectionFailedError
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
result2 = await hass.config_entries.flow.async_configure(
@ -243,7 +247,7 @@ async def test_form_cannot_connect_port(hass, mock_smile):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mock_smile.connect.side_effect = Smile.ConnectionFailedError
mock_smile.connect.side_effect = ConnectionFailedError
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
result2 = await hass.config_entries.flow.async_configure(

View File

@ -2,9 +2,9 @@
import asyncio
from Plugwise_Smile.Smile import Smile
from plugwise.exceptions import XMLDataMissingError
from homeassistant.components.plugwise import DOMAIN
from homeassistant.components.plugwise.const import DOMAIN
from homeassistant.config_entries import (
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_ERROR,
@ -43,7 +43,7 @@ async def test_smile_timeout(hass, mock_smile_notconnect):
async def test_smile_adam_xmlerror(hass, mock_smile_adam):
"""Detect malformed XML by Smile in Adam environment."""
mock_smile_adam.full_update_device.side_effect = Smile.XMLDataMissingError
mock_smile_adam.full_update_device.side_effect = XMLDataMissingError
entry = await async_init_integration(hass, mock_smile_adam)
assert entry.state == ENTRY_STATE_SETUP_RETRY

View File

@ -1,5 +1,7 @@
"""Tests for the Plugwise switch integration."""
from plugwise.exceptions import PlugwiseException
from homeassistant.config_entries import ENTRY_STATE_LOADED
from tests.components.plugwise.common import async_init_integration
@ -17,6 +19,31 @@ async def test_adam_climate_switch_entities(hass, mock_smile_adam):
assert str(state.state) == "on"
async def test_adam_climate_switch_negative_testing(hass, mock_smile_adam):
"""Test exceptions of climate related switch entities."""
mock_smile_adam.set_relay_state.side_effect = PlugwiseException
entry = await async_init_integration(hass, mock_smile_adam)
assert entry.state == ENTRY_STATE_LOADED
await hass.services.async_call(
"switch",
"turn_off",
{"entity_id": "switch.cv_pomp"},
blocking=True,
)
state = hass.states.get("switch.cv_pomp")
assert str(state.state) == "on"
await hass.services.async_call(
"switch",
"turn_on",
{"entity_id": "switch.fibaro_hc2"},
blocking=True,
)
state = hass.states.get("switch.fibaro_hc2")
assert str(state.state) == "on"
async def test_adam_climate_switch_changes(hass, mock_smile_adam):
"""Test changing of climate related switch entities."""
entry = await async_init_integration(hass, mock_smile_adam)