diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index f06e4fe70b7..d6d7a93a366 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -3,7 +3,7 @@ import asyncio from datetime import timedelta from math import ceil -from pyairvisual import Client +from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( AirVisualError, InvalidKeyError, @@ -211,23 +211,22 @@ def _standardize_node_pro_config_entry(hass, config_entry): async def async_setup_entry(hass, config_entry): """Set up AirVisual as config entry.""" - websession = aiohttp_client.async_get_clientsession(hass) - if CONF_API_KEY in config_entry.data: _standardize_geography_config_entry(hass, config_entry) - client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession) + websession = aiohttp_client.async_get_clientsession(hass) + cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession) async def async_update_data(): """Get new data from the API.""" if CONF_CITY in config_entry.data: - api_coro = client.api.city( + api_coro = cloud_api.air_quality.city( config_entry.data[CONF_CITY], config_entry.data[CONF_STATE], config_entry.data[CONF_COUNTRY], ) else: - api_coro = client.api.nearest_city( + api_coro = cloud_api.air_quality.nearest_city( config_entry.data[CONF_LATITUDE], config_entry.data[CONF_LONGITUDE], ) @@ -267,17 +266,13 @@ async def async_setup_entry(hass, config_entry): else: _standardize_node_pro_config_entry(hass, config_entry) - client = Client(session=websession) - async def async_update_data(): """Get new data from the API.""" try: - return await client.node.from_samba( - config_entry.data[CONF_IP_ADDRESS], - config_entry.data[CONF_PASSWORD], - include_history=False, - include_trends=False, - ) + async with NodeSamba( + config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD] + ) as node: + return await node.async_get_latest_measurements() except NodeProError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py index bb2d64a23db..047367fa67c 100644 --- a/homeassistant/components/airvisual/air_quality.py +++ b/homeassistant/components/airvisual/air_quality.py @@ -40,9 +40,9 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity): @property def air_quality_index(self): """Return the Air Quality Index (AQI).""" - if self.coordinator.data["current"]["settings"]["is_aqi_usa"]: - return self.coordinator.data["current"]["measurements"]["aqi_us"] - return self.coordinator.data["current"]["measurements"]["aqi_cn"] + if self.coordinator.data["settings"]["is_aqi_usa"]: + return self.coordinator.data["measurements"]["aqi_us"] + return self.coordinator.data["measurements"]["aqi_cn"] @property def available(self): @@ -52,61 +52,59 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity): @property def carbon_dioxide(self): """Return the CO2 (carbon dioxide) level.""" - return self.coordinator.data["current"]["measurements"].get("co2") + return self.coordinator.data["measurements"].get("co2") @property def device_info(self): """Return device registry information for this entity.""" return { - "identifiers": { - (DOMAIN, self.coordinator.data["current"]["serial_number"]) - }, - "name": self.coordinator.data["current"]["settings"]["node_name"], + "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, + "name": self.coordinator.data["settings"]["node_name"], "manufacturer": "AirVisual", - "model": f'{self.coordinator.data["current"]["status"]["model"]}', + "model": f'{self.coordinator.data["status"]["model"]}', "sw_version": ( - f'Version {self.coordinator.data["current"]["status"]["system_version"]}' - f'{self.coordinator.data["current"]["status"]["app_version"]}' + f'Version {self.coordinator.data["status"]["system_version"]}' + f'{self.coordinator.data["status"]["app_version"]}' ), } @property def name(self): """Return the name.""" - node_name = self.coordinator.data["current"]["settings"]["node_name"] + node_name = self.coordinator.data["settings"]["node_name"] return f"{node_name} Node/Pro: Air Quality" @property def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - return self.coordinator.data["current"]["measurements"].get("pm2_5") + return self.coordinator.data["measurements"].get("pm2_5") @property def particulate_matter_10(self): """Return the particulate matter 10 level.""" - return self.coordinator.data["current"]["measurements"].get("pm1_0") + return self.coordinator.data["measurements"].get("pm1_0") @property def particulate_matter_0_1(self): """Return the particulate matter 0.1 level.""" - return self.coordinator.data["current"]["measurements"].get("pm0_1") + return self.coordinator.data["measurements"].get("pm0_1") @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" - return self.coordinator.data["current"]["serial_number"] + return self.coordinator.data["serial_number"] @callback def update_from_latest_data(self): """Update the entity from the latest data.""" self._attrs.update( { - ATTR_VOC: self.coordinator.data["current"]["measurements"].get("voc"), + ATTR_VOC: self.coordinator.data["measurements"].get("voc"), **{ ATTR_SENSOR_LIFE.format(pollutant): lifespan - for pollutant, lifespan in self.coordinator.data["current"][ - "status" - ]["sensor_life"].items() + for pollutant, lifespan in self.coordinator.data["status"][ + "sensor_life" + ].items() }, } ) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index bb1c262eba7..d8ab508b8bc 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -1,7 +1,7 @@ """Define a config flow manager for AirVisual.""" import asyncio -from pyairvisual import Client +from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import InvalidKeyError, NodeProError import voluptuous as vol @@ -108,7 +108,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(session=websession, api_key=user_input[CONF_API_KEY]) + cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession) # If this is the first (and only the first) time we've seen this API key, check # that it's valid: @@ -120,7 +120,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async with check_keys_lock: if user_input[CONF_API_KEY] not in checked_keys: try: - await client.api.nearest_city() + await cloud_api.air_quality.nearest_city() except InvalidKeyError: return self.async_show_form( step_id="geography", @@ -157,16 +157,10 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self._async_set_unique_id(user_input[CONF_IP_ADDRESS]) - websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(session=websession) + node = NodeSamba(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD]) try: - await client.node.from_samba( - user_input[CONF_IP_ADDRESS], - user_input[CONF_PASSWORD], - include_history=False, - include_trends=False, - ) + await node.async_connect() except NodeProError as err: LOGGER.error("Error connecting to Node/Pro unit: %s", err) return self.async_show_form( @@ -175,6 +169,8 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors={CONF_IP_ADDRESS: "unable_to_connect"}, ) + await node.async_disconnect() + return self.async_create_entry( title=f"Node/Pro ({user_input[CONF_IP_ADDRESS]})", data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO}, diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index 93b57a4804e..d7824551275 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,6 +3,6 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==4.4.0"], + "requirements": ["pyairvisual==5.0.2"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index c67f31d0c3f..a81c118ecc9 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -225,22 +225,20 @@ class AirVisualNodeProSensor(AirVisualEntity): def device_info(self): """Return device registry information for this entity.""" return { - "identifiers": { - (DOMAIN, self.coordinator.data["current"]["serial_number"]) - }, - "name": self.coordinator.data["current"]["settings"]["node_name"], + "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, + "name": self.coordinator.data["settings"]["node_name"], "manufacturer": "AirVisual", - "model": f'{self.coordinator.data["current"]["status"]["model"]}', + "model": f'{self.coordinator.data["status"]["model"]}', "sw_version": ( - f'Version {self.coordinator.data["current"]["status"]["system_version"]}' - f'{self.coordinator.data["current"]["status"]["app_version"]}' + f'Version {self.coordinator.data["status"]["system_version"]}' + f'{self.coordinator.data["status"]["app_version"]}' ), } @property def name(self): """Return the name.""" - node_name = self.coordinator.data["current"]["settings"]["node_name"] + node_name = self.coordinator.data["settings"]["node_name"] return f"{node_name} Node/Pro: {self._name}" @property @@ -251,18 +249,14 @@ class AirVisualNodeProSensor(AirVisualEntity): @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self.coordinator.data['current']['serial_number']}_{self._kind}" + return f"{self.coordinator.data['serial_number']}_{self._kind}" @callback def update_from_latest_data(self): """Update the entity from the latest data.""" if self._kind == SENSOR_KIND_BATTERY_LEVEL: - self._state = self.coordinator.data["current"]["status"]["battery"] + self._state = self.coordinator.data["status"]["battery"] elif self._kind == SENSOR_KIND_HUMIDITY: - self._state = self.coordinator.data["current"]["measurements"].get( - "humidity" - ) + self._state = self.coordinator.data["measurements"].get("humidity") elif self._kind == SENSOR_KIND_TEMPERATURE: - self._state = self.coordinator.data["current"]["measurements"].get( - "temperature_C" - ) + self._state = self.coordinator.data["measurements"].get("temperature_C") diff --git a/requirements_all.txt b/requirements_all.txt index e6f59a668ec..feef14a2850 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1229,7 +1229,7 @@ pyaehw4a1==0.3.9 pyaftership==0.1.2 # homeassistant.components.airvisual -pyairvisual==4.4.0 +pyairvisual==5.0.2 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b196dc3eff..60269de34f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ py_nextbusnext==0.1.4 pyaehw4a1==0.3.9 # homeassistant.components.airvisual -pyairvisual==4.4.0 +pyairvisual==5.0.2 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index d365720ad26..2d0acf8fe7b 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -69,7 +69,7 @@ async def test_invalid_identifier(hass): } with patch( - "pyairvisual.api.API.nearest_city", + "pyairvisual.air_quality.AirQuality", side_effect=InvalidKeyError, ): result = await hass.config_entries.flow.async_init( @@ -96,7 +96,7 @@ async def test_migration(hass): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - with patch("pyairvisual.api.API.nearest_city"), patch.object( + with patch("pyairvisual.air_quality.AirQuality.nearest_city"), patch.object( hass.config_entries, "async_forward_entry_setup" ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: conf}) @@ -130,7 +130,7 @@ async def test_node_pro_error(hass): node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"} with patch( - "pyairvisual.node.Node.from_samba", + "pyairvisual.node.NodeSamba.async_connect", side_effect=NodeProError, ): result = await hass.config_entries.flow.async_init( @@ -185,7 +185,7 @@ async def test_step_geography(hass): with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True - ), patch("pyairvisual.api.API.nearest_city"): + ), patch("pyairvisual.air_quality.AirQuality.nearest_city"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=conf ) @@ -209,7 +209,7 @@ async def test_step_import(hass): with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True - ), patch("pyairvisual.api.API.nearest_city"): + ), patch("pyairvisual.air_quality.AirQuality.nearest_city"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf ) @@ -230,7 +230,11 @@ async def test_step_node_pro(hass): with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True - ), patch("pyairvisual.node.Node.from_samba"): + ), patch("pyairvisual.node.NodeSamba.async_connect"), patch( + "pyairvisual.node.NodeSamba.async_get_latest_measurements" + ), patch( + "pyairvisual.node.NodeSamba.async_disconnect" + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"} ) @@ -268,8 +272,8 @@ async def test_step_reauth(hass): assert result["step_id"] == "reauth_confirm" with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("pyairvisual.api.API.nearest_city"): + "homeassistant.components.airvisual.async_setup_entry", return_value=True + ), patch("pyairvisual.air_quality.AirQuality"): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: "defgh67890"} )