Add Atag One thermostat integration (#32361)
* add atag integration * ignore * generated * Update .gitignore * requirements update * update coveragerc * Revert "update coveragerc" * make entity_types more readable * add DOMAIN to listener * entity name * Use DataUpdateCoordinator * fix translations * enable preset_modes * fix water_heater * update coveragerc * remove scan_interval Co-Authored-By: J. Nick Koston <nick@koston.org> * Apply suggestions from code review Co-Authored-By: Martin Hjelmare <marhje52@gmail.com> * fixes review remarks * fix flake8 errors * ensure correct HVACmode * add away mode * use write_ha_state instead of refresh * remove OFF support, add Fahrenheit * rename test_config_flow.py Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
4a08c65205
commit
ccc3ce81f9
15 changed files with 644 additions and 0 deletions
|
@ -50,6 +50,10 @@ omit =
|
||||||
homeassistant/components/arwn/sensor.py
|
homeassistant/components/arwn/sensor.py
|
||||||
homeassistant/components/asterisk_cdr/mailbox.py
|
homeassistant/components/asterisk_cdr/mailbox.py
|
||||||
homeassistant/components/asterisk_mbox/*
|
homeassistant/components/asterisk_mbox/*
|
||||||
|
homeassistant/components/atag/__init__.py
|
||||||
|
homeassistant/components/atag/climate.py
|
||||||
|
homeassistant/components/atag/sensor.py
|
||||||
|
homeassistant/components/atag/water_heater.py
|
||||||
homeassistant/components/aten_pe/*
|
homeassistant/components/aten_pe/*
|
||||||
homeassistant/components/atome/*
|
homeassistant/components/atome/*
|
||||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||||
|
|
|
@ -35,6 +35,7 @@ homeassistant/components/arduino/* @fabaff
|
||||||
homeassistant/components/arest/* @fabaff
|
homeassistant/components/arest/* @fabaff
|
||||||
homeassistant/components/arris_tg2492lg/* @vanbalken
|
homeassistant/components/arris_tg2492lg/* @vanbalken
|
||||||
homeassistant/components/asuswrt/* @kennedyshead
|
homeassistant/components/asuswrt/* @kennedyshead
|
||||||
|
homeassistant/components/atag/* @MatsNL
|
||||||
homeassistant/components/aten_pe/* @mtdcr
|
homeassistant/components/aten_pe/* @mtdcr
|
||||||
homeassistant/components/atome/* @baqs
|
homeassistant/components/atome/* @baqs
|
||||||
homeassistant/components/august/* @bdraco
|
homeassistant/components/august/* @bdraco
|
||||||
|
|
20
homeassistant/components/atag/.translations/en.json
Normal file
20
homeassistant/components/atag/.translations/en.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Atag",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Connect to the device",
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port (10000)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"connection_error": "Failed to connect, please try again"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Only one Atag device can be added to Home Assistant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
259
homeassistant/components/atag/__init__.py
Normal file
259
homeassistant/components/atag/__init__.py
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
"""The ATAG Integration."""
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
|
from pyatag import AtagDataStore, AtagException
|
||||||
|
|
||||||
|
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
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
_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):
|
||||||
|
"""Set up the Atag component."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Set up Atag integration from a config entry."""
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
|
||||||
|
coordinator = AtagDataUpdateCoordinator(hass, session, entry)
|
||||||
|
|
||||||
|
await coordinator.async_refresh()
|
||||||
|
|
||||||
|
if not coordinator.last_update_success:
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
|
for platform in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class AtagDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Define an object to hold Atag data."""
|
||||||
|
|
||||||
|
def __init__(self, hass, session, entry):
|
||||||
|
"""Initialize."""
|
||||||
|
self.atag = AtagDataStore(session, paired=True, **entry.data)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self):
|
||||||
|
"""Update data via library."""
|
||||||
|
with async_timeout.timeout(20):
|
||||||
|
try:
|
||||||
|
await self.atag.async_update()
|
||||||
|
except (AtagException) as error:
|
||||||
|
raise UpdateFailed(error)
|
||||||
|
|
||||||
|
return self.atag.sensordata
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload Atag config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
|
for component in PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
class AtagEntity(Entity):
|
||||||
|
"""Defines a base Atag entity."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_type: dict) -> 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]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> dict:
|
||||||
|
"""Return info for device registry."""
|
||||||
|
device = self.coordinator.atag.device
|
||||||
|
version = self.coordinator.atag.apiversion
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, device)},
|
||||||
|
ATTR_NAME: "Atag Thermostat",
|
||||||
|
"model": "Atag One",
|
||||||
|
"sw_version": version,
|
||||||
|
"manufacturer": "Atag",
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""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."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self.coordinator.last_update_success
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique ID to use for this entity."""
|
||||||
|
return f"{self.coordinator.atag.device}-{self._id}"
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Connect to dispatcher listening for entity data notifications."""
|
||||||
|
self.async_on_remove(
|
||||||
|
self.coordinator.async_add_listener(self.async_write_ha_state)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update Atag entity."""
|
||||||
|
await self.coordinator.async_request_refresh()
|
106
homeassistant/components/atag/climate.py
Normal file
106
homeassistant/components/atag/climate.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
"""Initialization of ATAG One climate platform."""
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from homeassistant.components.climate import ClimateDevice
|
||||||
|
from homeassistant.components.climate.const import (
|
||||||
|
CURRENT_HVAC_HEAT,
|
||||||
|
CURRENT_HVAC_IDLE,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
|
HVAC_MODE_HEAT,
|
||||||
|
PRESET_AWAY,
|
||||||
|
PRESET_BOOST,
|
||||||
|
SUPPORT_PRESET_MODE,
|
||||||
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
|
|
||||||
|
from . import CLIMATE, DOMAIN, ENTITY_TYPES, AtagEntity
|
||||||
|
|
||||||
|
PRESET_SCHEDULE = "Auto"
|
||||||
|
PRESET_MANUAL = "Manual"
|
||||||
|
PRESET_EXTEND = "Extend"
|
||||||
|
SUPPORT_PRESET = [
|
||||||
|
PRESET_MANUAL,
|
||||||
|
PRESET_SCHEDULE,
|
||||||
|
PRESET_EXTEND,
|
||||||
|
PRESET_AWAY,
|
||||||
|
PRESET_BOOST,
|
||||||
|
]
|
||||||
|
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||||
|
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])])
|
||||||
|
|
||||||
|
|
||||||
|
class AtagThermostat(AtagEntity, ClimateDevice):
|
||||||
|
"""Atag climate device."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
return SUPPORT_FLAGS
|
||||||
|
|
||||||
|
@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
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_modes(self) -> List[str]:
|
||||||
|
"""Return the list of available hvac operation modes."""
|
||||||
|
return HVAC_MODES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_action(self) -> Optional[str]:
|
||||||
|
"""Return the current running hvac operation."""
|
||||||
|
if self.coordinator.atag.cv_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
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self) -> Optional[float]:
|
||||||
|
"""Return the current temperature."""
|
||||||
|
return self.coordinator.atag.temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self) -> Optional[float]:
|
||||||
|
"""Return the temperature we try to reach."""
|
||||||
|
return self.coordinator.atag.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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preset_modes(self) -> Optional[List[str]]:
|
||||||
|
"""Return a list of available preset modes."""
|
||||||
|
return SUPPORT_PRESET
|
||||||
|
|
||||||
|
async def async_set_temperature(self, **kwargs) -> None:
|
||||||
|
"""Set new target temperature."""
|
||||||
|
await self.coordinator.atag.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)
|
||||||
|
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)
|
||||||
|
self.async_write_ha_state()
|
50
homeassistant/components/atag/config_flow.py
Normal file
50
homeassistant/components/atag/config_flow.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"""Config flow for the Atag component."""
|
||||||
|
from pyatag import DEFAULT_PORT, AtagDataStore, AtagException
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from . import DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
|
DATA_SCHEMA = {
|
||||||
|
vol.Required(CONF_HOST): str,
|
||||||
|
vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Config flow for Atag."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
return await self._show_form()
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
try:
|
||||||
|
atag = AtagDataStore(session, **user_input)
|
||||||
|
await atag.async_check_pair_status()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def _show_form(self, errors=None):
|
||||||
|
"""Show the form to the user."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(DATA_SCHEMA),
|
||||||
|
errors=errors if errors else {},
|
||||||
|
)
|
8
homeassistant/components/atag/manifest.json
Normal file
8
homeassistant/components/atag/manifest.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"domain": "atag",
|
||||||
|
"name": "Atag",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/atag/",
|
||||||
|
"requirements": ["pyatag==0.2.18"],
|
||||||
|
"codeowners": ["@MatsNL"]
|
||||||
|
}
|
22
homeassistant/components/atag/sensor.py
Normal file
22
homeassistant/components/atag/sensor.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
"""Initialization of ATAG One sensor platform."""
|
||||||
|
from homeassistant.const import ATTR_STATE
|
||||||
|
|
||||||
|
from . import DOMAIN, ENTITY_TYPES, SENSOR, AtagEntity
|
||||||
|
|
||||||
|
|
||||||
|
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]:
|
||||||
|
entities.append(AtagSensor(coordinator, sensor))
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class AtagSensor(AtagEntity):
|
||||||
|
"""Representation of a AtagOne Sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.coordinator.data[self._id][ATTR_STATE]
|
20
homeassistant/components/atag/strings.json
Normal file
20
homeassistant/components/atag/strings.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Atag",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Connect to the device",
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port (10000)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"connection_error": "Failed to connect, please try again"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Only one Atag device can be added to Home Assistant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
homeassistant/components/atag/water_heater.py
Normal file
70
homeassistant/components/atag/water_heater.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
"""ATAG water heater component."""
|
||||||
|
from homeassistant.components.water_heater import (
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
STATE_ECO,
|
||||||
|
STATE_PERFORMANCE,
|
||||||
|
WaterHeaterDevice,
|
||||||
|
)
|
||||||
|
from homeassistant.const import STATE_OFF, TEMP_CELSIUS
|
||||||
|
|
||||||
|
from . import DOMAIN, ENTITY_TYPES, WATER_HEATER, AtagEntity
|
||||||
|
|
||||||
|
SUPPORT_FLAGS_HEATER = 0
|
||||||
|
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])])
|
||||||
|
|
||||||
|
|
||||||
|
class AtagWaterHeater(AtagEntity, WaterHeaterDevice):
|
||||||
|
"""Representation of an ATAG water heater."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
return SUPPORT_FLAGS_HEATER
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature_unit(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
return TEMP_CELSIUS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self):
|
||||||
|
"""Return the current temperature."""
|
||||||
|
return self.coordinator.atag.dhw_temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_operation(self):
|
||||||
|
"""Return current operation."""
|
||||||
|
if self.coordinator.atag.dhw_status:
|
||||||
|
return STATE_PERFORMANCE
|
||||||
|
return STATE_OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def operation_list(self):
|
||||||
|
"""List of available operation modes."""
|
||||||
|
return OPERATION_LIST
|
||||||
|
|
||||||
|
async def set_temperature(self, **kwargs):
|
||||||
|
"""Set new target 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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_temp(self):
|
||||||
|
"""Return the maximum temperature."""
|
||||||
|
return self.coordinator.atag.dhw_max_temp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_temp(self):
|
||||||
|
"""Return the minimum temperature."""
|
||||||
|
return self.coordinator.atag.dhw_min_temp
|
|
@ -13,6 +13,7 @@ FLOWS = [
|
||||||
"almond",
|
"almond",
|
||||||
"ambiclimate",
|
"ambiclimate",
|
||||||
"ambient_station",
|
"ambient_station",
|
||||||
|
"atag",
|
||||||
"august",
|
"august",
|
||||||
"axis",
|
"axis",
|
||||||
"braviatv",
|
"braviatv",
|
||||||
|
|
|
@ -1181,6 +1181,9 @@ pyalmond==0.0.2
|
||||||
# homeassistant.components.arlo
|
# homeassistant.components.arlo
|
||||||
pyarlo==0.2.3
|
pyarlo==0.2.3
|
||||||
|
|
||||||
|
# homeassistant.components.atag
|
||||||
|
pyatag==0.2.18
|
||||||
|
|
||||||
# homeassistant.components.netatmo
|
# homeassistant.components.netatmo
|
||||||
pyatmo==3.3.0
|
pyatmo==3.3.0
|
||||||
|
|
||||||
|
|
|
@ -478,6 +478,9 @@ pyalmond==0.0.2
|
||||||
# homeassistant.components.arlo
|
# homeassistant.components.arlo
|
||||||
pyarlo==0.2.3
|
pyarlo==0.2.3
|
||||||
|
|
||||||
|
# homeassistant.components.atag
|
||||||
|
pyatag==0.2.18
|
||||||
|
|
||||||
# homeassistant.components.netatmo
|
# homeassistant.components.netatmo
|
||||||
pyatmo==3.3.0
|
pyatmo==3.3.0
|
||||||
|
|
||||||
|
|
1
tests/components/atag/__init__.py
Normal file
1
tests/components/atag/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the Atag component."""
|
76
tests/components/atag/test_config_flow.py
Normal file
76
tests/components/atag/test_config_flow.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"""Tests for the Atag config flow."""
|
||||||
|
from unittest.mock import PropertyMock
|
||||||
|
|
||||||
|
from asynctest import patch
|
||||||
|
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 tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
FIXTURE_USER_INPUT = {
|
||||||
|
CONF_HOST: "127.0.0.1",
|
||||||
|
CONF_PORT: 10000,
|
||||||
|
}
|
||||||
|
FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy()
|
||||||
|
FIXTURE_COMPLETE_ENTRY[CONF_DEVICE] = "device_identifier"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_form(hass):
|
||||||
|
"""Test that the form is served with no input."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_one_config_allowed(hass):
|
||||||
|
"""Test that only one Atag configuration is allowed."""
|
||||||
|
MockConfigEntry(domain="atag", data=FIXTURE_USER_INPUT).add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
side_effect=AtagException(),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_USER},
|
||||||
|
data=FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {"base": "connection_error"}
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.atag.AtagDataStore.device",
|
||||||
|
new_callable=PropertyMock(return_value="device_identifier"),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_USER},
|
||||||
|
data=FIXTURE_USER_INPUT,
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == FIXTURE_COMPLETE_ENTRY[CONF_DEVICE]
|
||||||
|
assert result["data"] == FIXTURE_COMPLETE_ENTRY
|
Loading…
Add table
Reference in a new issue