From 8a2b34cc09a0cb993bcbe1195a316cfa1be7bfb2 Mon Sep 17 00:00:00 2001 From: Haemish Kyd Date: Sat, 11 Jul 2020 04:53:34 +0200 Subject: [PATCH] Updates to poolsense integration (#37613) * Created a binary sensor and corrected some review comments. * Updated the poolsense class and its interface to handle credentials better * Moved the client session to the PoolSense class. * Apply suggestions from code review * Update binary_sensor.py * Update homeassistant/components/poolsense/__init__.py * Update sensor.py * Update binary_sensor.py Co-authored-by: Chris Talkington --- .coveragerc | 1 + .../components/poolsense/__init__.py | 92 +++++++++++++------ .../components/poolsense/binary_sensor.py | 67 ++++++++++++++ .../components/poolsense/config_flow.py | 38 +++----- .../components/poolsense/manifest.json | 2 +- homeassistant/components/poolsense/sensor.py | 78 ++-------------- .../components/poolsense/strings.json | 4 +- .../components/poolsense/translations/en.json | 22 ++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 173 insertions(+), 135 deletions(-) create mode 100644 homeassistant/components/poolsense/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 2e42b7f4e6a..1293f8a71f9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -634,6 +634,7 @@ omit = homeassistant/components/point/* homeassistant/components/poolsense/__init__.py homeassistant/components/poolsense/sensor.py + homeassistant/components/poolsense/binary_sensor.py homeassistant/components/prezzibenzina/sensor.py homeassistant/components/proliphix/climate.py homeassistant/components/prometheus/* diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 472c09ffef9..bb41541b434 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -11,12 +11,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, update_coordinator -from homeassistant.helpers.update_coordinator import UpdateFailed +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -PLATFORMS = ["sensor"] +PLATFORMS = ["sensor", "binary_sensor"] + _LOGGER = logging.getLogger(__name__) @@ -30,20 +32,21 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up PoolSense from a config entry.""" - poolsense = PoolSense() - auth_valid = await poolsense.test_poolsense_credentials( + + poolsense = PoolSense( aiohttp_client.async_get_clientsession(hass), entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD], ) + auth_valid = await poolsense.test_poolsense_credentials() if not auth_valid: _LOGGER.error("Invalid authentication") return False - coordinator = await get_coordinator(hass, entry) + coordinator = PoolSenseDataUpdateCoordinator(hass, entry) - await hass.data[DOMAIN][entry.entry_id].async_refresh() + await coordinator.async_refresh() if not coordinator.last_update_success: raise ConfigEntryNotReady @@ -75,29 +78,64 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def get_coordinator(hass, entry): - """Get the data update coordinator.""" +class PoolSenseEntity(Entity): + """Implements a common class elements representing the PoolSense component.""" - async def async_get_data(): - _LOGGER.info("Run query to server") - poolsense = PoolSense() - return_data = {} + def __init__(self, coordinator, email, info_type): + """Initialize poolsense sensor.""" + self._unique_id = f"{email}-{info_type}" + self.coordinator = coordinator + self.info_type = info_type + + @property + def unique_id(self): + """Return a unique id.""" + return self._unique_id + + @property + def available(self): + """Return if sensor is available.""" + return self.coordinator.last_update_success + + @property + def should_poll(self) -> bool: + """Return the polling requirement of the entity.""" + return False + + async def async_added_to_hass(self) -> None: + """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) -> None: + """Request an update of the coordinator for entity.""" + await self.coordinator.async_request_refresh() + + +class PoolSenseDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold PoolSense data.""" + + def __init__(self, hass, entry): + """Initialize.""" + self.poolsense = PoolSense( + aiohttp_client.async_get_clientsession(hass), + entry.data[CONF_EMAIL], + entry.data[CONF_PASSWORD], + ) + self.hass = hass + self.entry = entry + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=timedelta(hours=1)) + + async def _async_update_data(self): + """Update data via library.""" + data = {} with async_timeout.timeout(10): try: - return_data = await poolsense.get_poolsense_data( - aiohttp_client.async_get_clientsession(hass), - entry.data[CONF_EMAIL], - entry.data[CONF_PASSWORD], - ) + data = await self.poolsense.get_poolsense_data() except (PoolSenseError) as error: + _LOGGER.error("PoolSense query did not complete.") raise UpdateFailed(error) - return return_data - - return update_coordinator.DataUpdateCoordinator( - hass, - logging.getLogger(__name__), - name=DOMAIN, - update_method=async_get_data, - update_interval=timedelta(hours=1), - ) + return data diff --git a/homeassistant/components/poolsense/binary_sensor.py b/homeassistant/components/poolsense/binary_sensor.py new file mode 100644 index 00000000000..6c3f5dc4cda --- /dev/null +++ b/homeassistant/components/poolsense/binary_sensor.py @@ -0,0 +1,67 @@ +"""Support for PoolSense binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) +from homeassistant.const import CONF_EMAIL + +from . import PoolSenseEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +BINARY_SENSORS = { + "pH Status": { + "unit": None, + "icon": None, + "name": "pH Status", + "device_class": DEVICE_CLASS_PROBLEM, + }, + "Chlorine Status": { + "unit": None, + "icon": None, + "name": "Chlorine Status", + "device_class": DEVICE_CLASS_PROBLEM, + }, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer sensor setup to the shared sensor module.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + binary_sensors_list = [] + for binary_sensor in BINARY_SENSORS: + binary_sensors_list.append( + PoolSenseBinarySensor( + coordinator, config_entry.data[CONF_EMAIL], binary_sensor + ) + ) + + async_add_entities(binary_sensors_list, False) + + +class PoolSenseBinarySensor(PoolSenseEntity, BinarySensorEntity): + """Representation of PoolSense binary sensors.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.coordinator.data[self.info_type] == "red" + + @property + def icon(self): + """Return the icon.""" + return BINARY_SENSORS[self.info_type]["icon"] + + @property + def device_class(self): + """Return the class of this device.""" + return BINARY_SENSORS[self.info_type]["device_class"] + + @property + def name(self): + """Return the name of the binary sensor.""" + return f"PoolSense {BINARY_SENSORS[self.info_type]['name']}" diff --git a/homeassistant/components/poolsense/config_flow.py b/homeassistant/components/poolsense/config_flow.py index 59fa8b5af0b..f6386fb34f5 100644 --- a/homeassistant/components/poolsense/config_flow.py +++ b/homeassistant/components/poolsense/config_flow.py @@ -19,12 +19,8 @@ class PoolSenseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - _options = None - def __init__(self): """Initialize PoolSense config flow.""" - self._email = None - self._password = None self._errors = {} async def async_step_user(self, user_input=None): @@ -35,37 +31,33 @@ class PoolSenseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(user_input[CONF_EMAIL]) self._abort_if_unique_id_configured() - self._email = user_input[CONF_EMAIL] - self._password = user_input[CONF_PASSWORD] - _LOGGER.debug("Configuring user: %s - Password hidden", self._email) - - poolsense = PoolSense() - api_key_valid = await poolsense.test_poolsense_credentials( - aiohttp_client.async_get_clientsession(self.hass), - self._email, - self._password, + _LOGGER.debug( + "Configuring user: %s - Password hidden", user_input[CONF_EMAIL] ) + poolsense = PoolSense( + aiohttp_client.async_get_clientsession(self.hass), + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + ) + api_key_valid = await poolsense.test_poolsense_credentials() + if not api_key_valid: self._errors["base"] = "invalid_auth" if not self._errors: return self.async_create_entry( - title=self._email, - data={CONF_EMAIL: self._email, CONF_PASSWORD: self._password}, + title=user_input[CONF_EMAIL], + data={ + CONF_EMAIL: user_input[CONF_EMAIL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, ) - return await self._show_setup_form(user_input, self._errors) - - async def _show_setup_form(self, user_input=None, errors=None): - """Show the setup form to the user.""" - if user_input is None: - user_input = {} - return self.async_show_form( step_id="user", data_schema=vol.Schema( {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} ), - errors=errors or {}, + errors=self._errors or {}, ) diff --git a/homeassistant/components/poolsense/manifest.json b/homeassistant/components/poolsense/manifest.json index b50ed277170..9eebadf2da0 100644 --- a/homeassistant/components/poolsense/manifest.json +++ b/homeassistant/components/poolsense/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/poolsense", "requirements": [ - "poolsense==0.0.5" + "poolsense==0.0.8" ], "codeowners": [ "@haemishkyd" diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py index c50a8cb6b26..098db73c258 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -7,13 +7,12 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, - STATE_OK, - STATE_PROBLEM, TEMP_CELSIUS, UNIT_PERCENTAGE, ) from homeassistant.helpers.entity import Entity +from . import PoolSenseEntity from .const import ATTRIBUTION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,7 +27,7 @@ SENSORS = { "pH": {"unit": None, "icon": "mdi:pool", "name": "pH", "device_class": None}, "Battery": { "unit": UNIT_PERCENTAGE, - "icon": "mdi:battery", + "icon": None, "name": "Battery", "device_class": DEVICE_CLASS_BATTERY, }, @@ -68,18 +67,6 @@ SENSORS = { "name": "pH Low", "device_class": None, }, - "pH Status": { - "unit": None, - "icon": "mdi:pool", - "name": "pH Status", - "device_class": None, - }, - "Chlorine Status": { - "unit": None, - "icon": "mdi:pool", - "name": "Chlorine Status", - "device_class": None, - }, } @@ -87,53 +74,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Defer sensor setup to the shared sensor module.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - PoolSenseSensor(coordinator, config_entry.data[CONF_EMAIL], info_type) - for info_type in SENSORS - ) + sensors_list = [] + for sensor in SENSORS: + sensors_list.append( + PoolSenseSensor(coordinator, config_entry.data[CONF_EMAIL], sensor) + ) + + async_add_entities(sensors_list, False) -class PoolSenseSensor(Entity): +class PoolSenseSensor(PoolSenseEntity, Entity): """Sensor representing poolsense data.""" - def __init__(self, coordinator, email, info_type): - """Initialize poolsense sensor.""" - self._email = email - self._unique_id = f"{email}-{info_type}" - self.coordinator = coordinator - self.info_type = info_type - - @property - def available(self): - """Return if sensor is available.""" - return self.coordinator.last_update_success - - @property - def unique_id(self): - """Return a unique ID to use for this sensor.""" - return self._unique_id - @property def name(self): """Return the name of the particular component.""" return f"PoolSense {SENSORS[self.info_type]['name']}" - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - @property def state(self): """State of the sensor.""" - if self.info_type == "pH Status": - if self.coordinator.data[self.info_type] == "red": - return STATE_PROBLEM - return STATE_OK - if self.info_type == "Chlorine Status": - if self.coordinator.data[self.info_type] == "red": - return STATE_PROBLEM - return STATE_OK return self.coordinator.data[self.info_type] @property @@ -144,14 +104,6 @@ class PoolSenseSensor(Entity): @property def icon(self): """Return the icon.""" - if self.info_type == "pH Status": - if self.coordinator.data[self.info_type] == "red": - return "mdi:thumb-down" - return "mdi:thumb-up" - if self.info_type == "Chlorine Status": - if self.coordinator.data[self.info_type] == "red": - return "mdi:thumb-down" - return "mdi:thumb-up" return SENSORS[self.info_type]["icon"] @property @@ -163,13 +115,3 @@ class PoolSenseSensor(Entity): def device_state_attributes(self): """Return device attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - - async def async_update(self): - """Update status of sensor.""" - await self.coordinator.async_request_refresh() - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) diff --git a/homeassistant/components/poolsense/strings.json b/homeassistant/components/poolsense/strings.json index 21a1ab393a4..d26a44cc275 100644 --- a/homeassistant/components/poolsense/strings.json +++ b/homeassistant/components/poolsense/strings.json @@ -11,9 +11,7 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/poolsense/translations/en.json b/homeassistant/components/poolsense/translations/en.json index a38fca9ed48..9104c9a6268 100644 --- a/homeassistant/components/poolsense/translations/en.json +++ b/homeassistant/components/poolsense/translations/en.json @@ -1,22 +1,22 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, "step": { "user": { + "title": "PoolSense", + "description": "Set up PoolSense integration. Register on the dedicated app to get your username and password. Serial is optional.", "data": { "email": "Email", "password": "Password" - }, - "description": "[%key:common::config_flow::description%]", - "title": "PoolSense" + } } + }, + "error": { + "cannot_connect": "Can't connect to PoolSense.", + "invalid_auth": "Invalid authorisation details.", + "unknown": "Unknown Error." + }, + "abort": { + "already_configured": "Device already configured." } } } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 223a6ec05b9..de6b0ca6efd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1111,7 +1111,7 @@ pmsensor==0.4 pocketcasts==0.1 # homeassistant.components.poolsense -poolsense==0.0.5 +poolsense==0.0.8 # homeassistant.components.reddit praw==6.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3a3a7a1abe..2eeffea0e55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -503,7 +503,7 @@ plumlightpad==0.0.11 pmsensor==0.4 # homeassistant.components.poolsense -poolsense==0.0.5 +poolsense==0.0.8 # homeassistant.components.reddit praw==6.5.1