Rewrite Atag (#35496)

* rewrite library

* Update strings.json

* fix updated with empty reply

* dont use entity_id

* atag_id

* use super init instead

* original ids to prevent breaking change
This commit is contained in:
MatsNl 2020-05-12 11:47:33 +02:00 committed by GitHub
parent 86b984b0bd
commit 4099815a08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 112 additions and 173 deletions

View file

@ -3,24 +3,12 @@ from datetime import timedelta
import logging
import async_timeout
from pyatag import AtagDataStore, AtagException
from pyatag import AtagException, AtagOne
from homeassistant.components.climate import DOMAIN as CLIMATE
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.water_heater import DOMAIN as WATER_HEATER
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
ATTR_ID,
ATTR_MODE,
ATTR_NAME,
ATTR_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
PRESSURE_BAR,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant, asyncio
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -30,94 +18,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
_LOGGER = logging.getLogger(__name__)
DOMAIN = "atag"
DATA_LISTENER = f"{DOMAIN}_listener"
SIGNAL_UPDATE_ATAG = f"{DOMAIN}_update"
PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR]
HOUR = "h"
FIRE = "fire"
PERCENTAGE = "%"
ICONS = {
TEMP_CELSIUS: "mdi:thermometer",
PRESSURE_BAR: "mdi:gauge",
FIRE: "mdi:fire",
ATTR_MODE: "mdi:settings",
}
ENTITY_TYPES = {
SENSOR: [
{
ATTR_NAME: "Outside Temperature",
ATTR_ID: "outside_temp",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: ICONS[TEMP_CELSIUS],
},
{
ATTR_NAME: "Average Outside Temperature",
ATTR_ID: "tout_avg",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: ICONS[TEMP_CELSIUS],
},
{
ATTR_NAME: "Weather Status",
ATTR_ID: "weather_status",
ATTR_UNIT_OF_MEASUREMENT: None,
ATTR_DEVICE_CLASS: None,
ATTR_ICON: None,
},
{
ATTR_NAME: "CH Water Pressure",
ATTR_ID: "ch_water_pres",
ATTR_UNIT_OF_MEASUREMENT: PRESSURE_BAR,
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
ATTR_ICON: ICONS[PRESSURE_BAR],
},
{
ATTR_NAME: "CH Water Temperature",
ATTR_ID: "ch_water_temp",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: ICONS[TEMP_CELSIUS],
},
{
ATTR_NAME: "CH Return Temperature",
ATTR_ID: "ch_return_temp",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: ICONS[TEMP_CELSIUS],
},
{
ATTR_NAME: "Burning Hours",
ATTR_ID: "burning_hours",
ATTR_UNIT_OF_MEASUREMENT: HOUR,
ATTR_DEVICE_CLASS: None,
ATTR_ICON: ICONS[FIRE],
},
{
ATTR_NAME: "Flame",
ATTR_ID: "rel_mod_level",
ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE,
ATTR_DEVICE_CLASS: None,
ATTR_ICON: ICONS[FIRE],
},
],
CLIMATE: {
ATTR_NAME: DOMAIN.title(),
ATTR_ID: CLIMATE,
ATTR_UNIT_OF_MEASUREMENT: None,
ATTR_DEVICE_CLASS: None,
ATTR_ICON: None,
},
WATER_HEATER: {
ATTR_NAME: DOMAIN.title(),
ATTR_ID: WATER_HEATER,
ATTR_UNIT_OF_MEASUREMENT: None,
ATTR_DEVICE_CLASS: None,
ATTR_ICON: None,
},
}
async def async_setup(hass: HomeAssistant, config):
@ -130,8 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
session = async_get_clientsession(hass)
coordinator = AtagDataUpdateCoordinator(hass, session, entry)
await coordinator.async_refresh()
try:
await coordinator.async_refresh()
except AtagException:
raise ConfigEntryNotReady
if not coordinator.last_update_success:
raise ConfigEntryNotReady
@ -152,7 +55,7 @@ class AtagDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass, session, entry):
"""Initialize."""
self.atag = AtagDataStore(session, paired=True, **entry.data)
self.atag = AtagOne(session=session, **entry.data)
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30)
@ -162,12 +65,12 @@ class AtagDataUpdateCoordinator(DataUpdateCoordinator):
"""Update data via library."""
with async_timeout.timeout(20):
try:
await self.atag.async_update()
if not self.atag.sensordata:
await self.atag.update()
if not self.atag.report:
raise UpdateFailed("No data")
except (AtagException) as error:
except AtagException as error:
raise UpdateFailed(error)
return self.atag.sensordata
return self.atag.report
async def async_unload_entry(hass, entry):
@ -188,24 +91,21 @@ async def async_unload_entry(hass, entry):
class AtagEntity(Entity):
"""Defines a base Atag entity."""
def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_type: dict) -> None:
def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_id: str) -> None:
"""Initialize the Atag entity."""
self.coordinator = coordinator
self._id = atag_type[ATTR_ID]
self._name = atag_type[ATTR_NAME]
self._icon = atag_type[ATTR_ICON]
self._unit = atag_type[ATTR_UNIT_OF_MEASUREMENT]
self._class = atag_type[ATTR_DEVICE_CLASS]
self._id = atag_id
self._name = DOMAIN.title()
@property
def device_info(self) -> dict:
"""Return info for device registry."""
device = self.coordinator.atag.device
device = self.coordinator.atag.id
version = self.coordinator.atag.apiversion
return {
"identifiers": {(DOMAIN, device)},
ATTR_NAME: "Atag Thermostat",
"name": "Atag Thermostat",
"model": "Atag One",
"sw_version": version,
"manufacturer": "Atag",
@ -216,14 +116,6 @@ class AtagEntity(Entity):
"""Return the name of the entity."""
return self._name
@property
def icon(self) -> str:
"""Return the mdi icon of the entity."""
self._icon = (
self.coordinator.data.get(self._id, {}).get(ATTR_ICON) or self._icon
)
return self._icon
@property
def should_poll(self) -> bool:
"""Return the polling requirement of the entity."""
@ -232,12 +124,7 @@ class AtagEntity(Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit
@property
def device_class(self):
"""Return the device class."""
return self._class
return self.coordinator.atag.climate.temp_unit
@property
def available(self):
@ -247,7 +134,7 @@ class AtagEntity(Entity):
@property
def unique_id(self):
"""Return a unique ID to use for this entity."""
return f"{self.coordinator.atag.device}-{self._id}"
return f"{self.coordinator.atag.id}-{self._id}"
async def async_added_to_hass(self):
"""Connect to dispatcher listening for entity data notifications."""

View file

@ -1,7 +1,7 @@
"""Initialization of ATAG One climate platform."""
from typing import List, Optional
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
@ -14,7 +14,7 @@ from homeassistant.components.climate.const import (
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from . import CLIMATE, DOMAIN, ENTITY_TYPES, AtagEntity
from . import CLIMATE, DOMAIN, AtagEntity
PRESET_SCHEDULE = "Auto"
PRESET_MANUAL = "Manual"
@ -33,10 +33,10 @@ HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT]
async def async_setup_entry(hass, entry, async_add_entities):
"""Load a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([AtagThermostat(coordinator, ENTITY_TYPES[CLIMATE])])
async_add_entities([AtagThermostat(coordinator, CLIMATE)])
class AtagThermostat(AtagEntity, ClimateDevice):
class AtagThermostat(AtagEntity, ClimateEntity):
"""Atag climate device."""
@property
@ -47,8 +47,8 @@ class AtagThermostat(AtagEntity, ClimateDevice):
@property
def hvac_mode(self) -> Optional[str]:
"""Return hvac operation ie. heat, cool mode."""
if self.coordinator.atag.hvac_mode in HVAC_MODES:
return self.coordinator.atag.hvac_mode
if self.coordinator.atag.climate.hvac_mode in HVAC_MODES:
return self.coordinator.atag.climate.hvac_mode
return None
@property
@ -59,31 +59,31 @@ class AtagThermostat(AtagEntity, ClimateDevice):
@property
def hvac_action(self) -> Optional[str]:
"""Return the current running hvac operation."""
if self.coordinator.atag.cv_status:
if self.coordinator.atag.climate.status:
return CURRENT_HVAC_HEAT
return CURRENT_HVAC_IDLE
@property
def temperature_unit(self):
"""Return the unit of measurement."""
if self.coordinator.atag.temp_unit in [TEMP_CELSIUS, TEMP_FAHRENHEIT]:
return self.coordinator.atag.temp_unit
if self.coordinator.atag.climate.temp_unit in [TEMP_CELSIUS, TEMP_FAHRENHEIT]:
return self.coordinator.atag.climate.temp_unit
return None
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
return self.coordinator.atag.temperature
return self.coordinator.atag.climate.temperature
@property
def target_temperature(self) -> Optional[float]:
"""Return the temperature we try to reach."""
return self.coordinator.atag.target_temperature
return self.coordinator.atag.climate.target_temperature
@property
def preset_mode(self) -> Optional[str]:
"""Return the current preset mode, e.g., auto, manual, fireplace, extend, etc."""
return self.coordinator.atag.hold_mode
return self.coordinator.atag.climate.preset_mode
@property
def preset_modes(self) -> Optional[List[str]]:
@ -92,15 +92,15 @@ class AtagThermostat(AtagEntity, ClimateDevice):
async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
await self.coordinator.atag.set_temp(kwargs.get(ATTR_TEMPERATURE))
await self.coordinator.atag.climate.set_temp(kwargs.get(ATTR_TEMPERATURE))
self.async_write_ha_state()
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode."""
await self.coordinator.atag.set_hvac_mode(hvac_mode)
await self.coordinator.atag.climate.set_hvac_mode(hvac_mode)
self.async_write_ha_state()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
await self.coordinator.atag.set_hold_mode(preset_mode)
await self.coordinator.atag.climate.set_preset_mode(preset_mode)
self.async_write_ha_state()

View file

@ -1,9 +1,9 @@
"""Config flow for the Atag component."""
from pyatag import DEFAULT_PORT, AtagDataStore, AtagException
from pyatag import DEFAULT_PORT, AtagException, AtagOne
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT
from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -11,6 +11,7 @@ from . import DOMAIN # pylint: disable=unused-import
DATA_SCHEMA = {
vol.Required(CONF_HOST): str,
vol.Optional(CONF_EMAIL): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int),
}
@ -31,14 +32,15 @@ class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self._show_form()
session = async_get_clientsession(self.hass)
try:
atag = AtagDataStore(session, **user_input)
await atag.async_check_pair_status()
atag = AtagOne(session=session, **user_input)
await atag.authorize()
await atag.update(force=True)
except AtagException:
return await self._show_form({"base": "connection_error"})
user_input.update({CONF_DEVICE: atag.device})
return self.async_create_entry(title=atag.device, data=user_input)
user_input.update({CONF_DEVICE: atag.id})
return self.async_create_entry(title=atag.id, data=user_input)
@callback
async def _show_form(self, errors=None):

View file

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

View file

@ -1,14 +1,31 @@
"""Initialization of ATAG One sensor platform."""
from homeassistant.const import ATTR_STATE
from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
PRESSURE_BAR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from . import DOMAIN, ENTITY_TYPES, SENSOR, AtagEntity
from . import DOMAIN, AtagEntity
SENSORS = {
"Outside Temperature": "outside_temp",
"Average Outside Temperature": "tout_avg",
"Weather Status": "weather_status",
"CH Water Pressure": "ch_water_pres",
"CH Water Temperature": "ch_water_temp",
"CH Return Temperature": "ch_return_temp",
"Burning Hours": "burning_hours",
"Flame": "rel_mod_level",
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Initialize sensor platform from config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
entities = []
for sensor in ENTITY_TYPES[SENSOR]:
for sensor in SENSORS:
entities.append(AtagSensor(coordinator, sensor))
async_add_entities(entities)
@ -16,7 +33,38 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AtagSensor(AtagEntity):
"""Representation of a AtagOne Sensor."""
def __init__(self, coordinator, sensor):
"""Initialize Atag sensor."""
super().__init__(coordinator, SENSORS[sensor])
self._name = sensor
@property
def state(self):
"""Return the state of the sensor."""
return self.coordinator.data[self._id][ATTR_STATE]
return self.coordinator.data[self._id].state
@property
def icon(self):
"""Return icon."""
return self.coordinator.data[self._id].icon
@property
def device_class(self):
"""Return deviceclass."""
if self.coordinator.data[self._id].sensorclass in [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
]:
return self.coordinator.data[self._id].sensorclass
return None
@property
def unit_of_measurement(self):
"""Return measure."""
if self.coordinator.data[self._id].measure in [
PRESSURE_BAR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
]:
return self.coordinator.data[self._id].measure
return None

View file

@ -6,6 +6,7 @@
"title": "Connect to the device",
"data": {
"host": "Host",
"email": "Email (Optional)",
"port": "Port (10000)"
}
}

View file

@ -3,11 +3,11 @@ from homeassistant.components.water_heater import (
ATTR_TEMPERATURE,
STATE_ECO,
STATE_PERFORMANCE,
WaterHeaterDevice,
WaterHeaterEntity,
)
from homeassistant.const import STATE_OFF, TEMP_CELSIUS
from . import DOMAIN, ENTITY_TYPES, WATER_HEATER, AtagEntity
from . import DOMAIN, WATER_HEATER, AtagEntity
SUPPORT_FLAGS_HEATER = 0
OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE]
@ -16,10 +16,10 @@ OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE]
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Initialize DHW device from config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities([AtagWaterHeater(coordinator, ENTITY_TYPES[WATER_HEATER])])
async_add_entities([AtagWaterHeater(coordinator, WATER_HEATER)])
class AtagWaterHeater(AtagEntity, WaterHeaterDevice):
class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
"""Representation of an ATAG water heater."""
@property
@ -35,12 +35,12 @@ class AtagWaterHeater(AtagEntity, WaterHeaterDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
return self.coordinator.atag.dhw_temperature
return self.coordinator.atag.dhw.temperature
@property
def current_operation(self):
"""Return current operation."""
if self.coordinator.atag.dhw_status:
if self.coordinator.atag.dhw.status:
return STATE_PERFORMANCE
return STATE_OFF
@ -51,20 +51,20 @@ class AtagWaterHeater(AtagEntity, WaterHeaterDevice):
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
if await self.coordinator.atag.dhw_set_temp(kwargs.get(ATTR_TEMPERATURE)):
if await self.coordinator.atag.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)):
self.async_write_ha_state()
@property
def target_temperature(self):
"""Return the setpoint if water demand, otherwise return base temp (comfort level)."""
return self.coordinator.atag.dhw_target_temperature
return self.coordinator.atag.dhw.target_temperature
@property
def max_temp(self):
"""Return the maximum temperature."""
return self.coordinator.atag.dhw_max_temp
return self.coordinator.atag.dhw.max_temp
@property
def min_temp(self):
"""Return the minimum temperature."""
return self.coordinator.atag.dhw_min_temp
return self.coordinator.atag.dhw.min_temp

View file

@ -1212,7 +1212,7 @@ pyalmond==0.0.2
pyarlo==0.2.3
# homeassistant.components.atag
pyatag==0.2.19
pyatag==0.3.1.1
# homeassistant.components.netatmo
pyatmo==3.3.1

View file

@ -515,7 +515,7 @@ pyalmond==0.0.2
pyarlo==0.2.3
# homeassistant.components.atag
pyatag==0.2.19
pyatag==0.3.1.1
# homeassistant.components.netatmo
pyatmo==3.3.1

View file

@ -3,13 +3,14 @@ from pyatag import AtagException
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.atag import DOMAIN
from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT
from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT
from tests.async_mock import PropertyMock, patch
from tests.common import MockConfigEntry
FIXTURE_USER_INPUT = {
CONF_HOST: "127.0.0.1",
CONF_EMAIL: "test@domain.com",
CONF_PORT: 10000,
}
FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy()
@ -42,7 +43,7 @@ async def test_connection_error(hass):
"""Test we show user form on Atag connection error."""
with patch(
"homeassistant.components.atag.config_flow.AtagDataStore.async_check_pair_status",
"homeassistant.components.atag.config_flow.AtagOne.authorize",
side_effect=AtagException(),
):
result = await hass.config_entries.flow.async_init(
@ -58,10 +59,10 @@ async def test_connection_error(hass):
async def test_full_flow_implementation(hass):
"""Test registering an integration and finishing flow works."""
with patch(
"homeassistant.components.atag.AtagDataStore.async_check_pair_status",
with patch("homeassistant.components.atag.AtagOne.authorize",), patch(
"homeassistant.components.atag.AtagOne.update",
), patch(
"homeassistant.components.atag.AtagDataStore.device",
"homeassistant.components.atag.AtagOne.id",
new_callable=PropertyMock(return_value="device_identifier"),
):
result = await hass.config_entries.flow.async_init(