async def async_step_geography_finish(self, user_input, error_step, error_schema):
        """Validate a Cloud API key."""
        websession = aiohttp_client.async_get_clientsession(self.hass)
        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:
        valid_keys = self.hass.data.setdefault("airvisual_checked_api_keys", set())
        valid_keys_lock = self.hass.data.setdefault(
            "airvisual_checked_api_keys_lock", asyncio.Lock()
        )

        async with valid_keys_lock:
            if user_input[CONF_API_KEY] not in valid_keys:
                try:
                    await cloud_api.air_quality.nearest_city()
                except InvalidKeyError:
                    return self.async_show_form(
                        step_id=error_step,
                        data_schema=error_schema,
                        errors={CONF_API_KEY: "invalid_api_key"},
                    )

                valid_keys.add(user_input[CONF_API_KEY])

        existing_entry = await self.async_set_unique_id(self._geo_id)
        if existing_entry:
            self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
            return self.async_abort(reason="reauth_successful")

        return self.async_create_entry(
            title=f"Cloud API ({self._geo_id})",
            data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY},
        )
Esempio n. 2
0
    async def _async_finish_geography(
        self, user_input: dict[str, str], integration_type: str
    ) -> FlowResult:
        """Validate a Cloud API key."""
        websession = aiohttp_client.async_get_clientsession(self.hass)
        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:
        valid_keys = self.hass.data.setdefault("airvisual_checked_api_keys", set())
        valid_keys_lock = self.hass.data.setdefault(
            "airvisual_checked_api_keys_lock", asyncio.Lock()
        )

        if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
            coro = cloud_api.air_quality.nearest_city()
            error_schema = self.geography_coords_schema
            error_step = "geography_by_coords"
        else:
            coro = cloud_api.air_quality.city(
                user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY]
            )
            error_schema = GEOGRAPHY_NAME_SCHEMA
            error_step = "geography_by_name"

        async with valid_keys_lock:
            if user_input[CONF_API_KEY] not in valid_keys:
                try:
                    await coro
                except InvalidKeyError:
                    return self.async_show_form(
                        step_id=error_step,
                        data_schema=error_schema,
                        errors={CONF_API_KEY: "invalid_api_key"},
                    )
                except NotFoundError:
                    return self.async_show_form(
                        step_id=error_step,
                        data_schema=error_schema,
                        errors={CONF_CITY: "location_not_found"},
                    )
                except AirVisualError as err:
                    LOGGER.error(err)
                    return self.async_show_form(
                        step_id=error_step,
                        data_schema=error_schema,
                        errors={"base": "unknown"},
                    )

                valid_keys.add(user_input[CONF_API_KEY])

        existing_entry = await self.async_set_unique_id(self._geo_id)
        if existing_entry:
            self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
            return self.async_abort(reason="reauth_successful")

        return self.async_create_entry(
            title=f"Cloud API ({self._geo_id})",
            data={**user_input, CONF_INTEGRATION_TYPE: integration_type},
        )
Esempio n. 3
0
    async def _async_get_air_quality(self) -> AirQuality:
        cloud_api = CloudAPI(self.api_key)

        # ...or get it explicitly:
        data = await cloud_api.air_quality.nearest_city(
            latitude=self.latitude, longitude=self.longitude)
        aqi = data['current']['pollution']['aqius']
        return self.convert_aqi(aqi)
Esempio n. 4
0
async def test_node_not_found(aresponses):
    """Test that the proper error is raised when no Pro node is found."""
    aresponses.add(
        "www.airvisual.com",
        "/api/v2/node/12345",
        "get",
        aresponses.Response(
            text='"node not found"',
            headers={"Content-Type": "application/json"},
            status=200,
        ),
    )

    with pytest.raises(NodeProError):
        async with ClientSession() as session:
            cloud_api = CloudAPI(TEST_API_KEY, session=session)
            await cloud_api.node.get_by_node_id("12345")
Esempio n. 5
0
async def test_cities(aresponses):
    """Test getting a list of supported cities."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/cities",
        "get",
        aresponses.Response(
            text=load_fixture("cities_response.json"),
            headers={"Content-Type": "application/json"},
            status=200,
        ),
    )

    async with aiohttp.ClientSession() as session:
        cloud_api = CloudAPI(TEST_API_KEY, session=session)
        data = await cloud_api.supported.cities(TEST_COUNTRY, TEST_STATE)
        assert len(data) == 27
Esempio n. 6
0
async def test_non_json_response(aresponses):
    """Test that the proper error is raised when the response text isn't JSON."""
    aresponses.add(
        "www.airvisual.com",
        "/api/v2/node/12345",
        "get",
        aresponses.Response(
            text="This is a valid response, but it isn't JSON.",
            headers={"Content-Type": "application/json"},
            status=200,
        ),
    )

    with pytest.raises(AirVisualError):
        async with ClientSession() as session:
            cloud_api = CloudAPI(TEST_API_KEY, session=session)
            await cloud_api.node.get_by_node_id("12345")
Esempio n. 7
0
async def test_permission_denied(aresponses):
    """Test that the proper error is raised when permission is denied."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/nearest_station",
        "get",
        aresponses.Response(
            text=load_fixture("error_permission_denied_response.json"),
            headers={"Content-Type": "application/json"},
            status=401,
        ),
    )

    with pytest.raises(UnauthorizedError):
        async with ClientSession() as session:
            cloud_api = CloudAPI(TEST_API_KEY, session=session)
            await cloud_api.air_quality.nearest_station()
Esempio n. 8
0
async def test_city_not_found(aresponses):
    """Test that the proper error is raised when a city cannot be found."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/nearest_city",
        "get",
        aresponses.Response(
            text=load_fixture("error_city_not_found_response.json"),
            headers={"Content-Type": "application/json"},
            status=401,
        ),
    )

    with pytest.raises(NotFoundError):
        async with ClientSession() as session:
            cloud_api = CloudAPI(TEST_API_KEY, session=session)
            await cloud_api.air_quality.nearest_city()
Esempio n. 9
0
async def test_generic_error(aresponses):
    """Test that a generic error is raised appropriately."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/nearest_city",
        "get",
        aresponses.Response(
            text=load_fixture("error_generic_response.json"),
            headers={"Content-Type": "application/json"},
            status=401,
        ),
    )

    with pytest.raises(AirVisualError):
        async with ClientSession() as session:
            cloud_api = CloudAPI(TEST_API_KEY, session=session)
            await cloud_api.air_quality.nearest_city()
Esempio n. 10
0
async def test_incorrect_api_key(aresponses):
    """Test that the proper error is raised with an incorrect API key."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/nearest_city",
        "get",
        aresponses.Response(
            text=load_fixture("error_incorrect_api_key_response.json"),
            headers={"Content-Type": "application/json"},
            status=401,
        ),
    )

    with pytest.raises(InvalidKeyError):
        async with ClientSession() as session:
            cloud_api = CloudAPI(TEST_API_KEY, session=session)
            await cloud_api.air_quality.nearest_city()
Esempio n. 11
0
async def test_city_by_ip(aresponses):
    """Test getting the nearest city by IP address."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/nearest_city",
        "get",
        aresponses.Response(
            text=load_fixture("city_response.json"),
            headers={"Content-Type": "application/json"},
            status=200,
        ),
    )

    async with aiohttp.ClientSession() as session:
        cloud_api = CloudAPI(TEST_API_KEY, session=session)
        data = await cloud_api.air_quality.nearest_city()
        assert data["city"] == "Los Angeles"
        assert data["state"] == "California"
        assert data["country"] == "USA"
Esempio n. 12
0
async def test_no_explicit_client_session(aresponses):
    """Test not explicitly providing an aiohttp ClientSession."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/city_ranking",
        "get",
        aresponses.Response(
            text=load_fixture("city_ranking_response.json"),
            headers={"Content-Type": "application/json"},
            status=200,
        ),
    )

    cloud_api = CloudAPI(TEST_API_KEY)
    data = await cloud_api.air_quality.ranking()
    assert len(data) == 3
    assert data[0]["city"] == "Portland"
    assert data[0]["state"] == "Oregon"
    assert data[0]["country"] == "USA"
Esempio n. 13
0
    async def async_step_geography(self, user_input=None):
        """Handle the initialization of the integration via the cloud API."""
        if not user_input:
            return self.async_show_form(step_id="geography",
                                        data_schema=self.geography_schema)

        self._geo_id = async_get_geography_id(user_input)
        await self._async_set_unique_id(self._geo_id)
        self._abort_if_unique_id_configured()

        # Find older config entries without unique ID:
        for entry in self._async_current_entries():
            if entry.version != 1:
                continue

            if any(self._geo_id == async_get_geography_id(geography)
                   for geography in entry.data[CONF_GEOGRAPHIES]):
                return self.async_abort(reason="already_configured")

        websession = aiohttp_client.async_get_clientsession(self.hass)
        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:
        checked_keys = self.hass.data.setdefault("airvisual_checked_api_keys",
                                                 set())
        check_keys_lock = self.hass.data.setdefault(
            "airvisual_checked_api_keys_lock", asyncio.Lock())

        async with check_keys_lock:
            if user_input[CONF_API_KEY] not in checked_keys:
                try:
                    await cloud_api.air_quality.nearest_city()
                except InvalidKeyError:
                    return self.async_show_form(
                        step_id="geography",
                        data_schema=self.geography_schema,
                        errors={CONF_API_KEY: "invalid_api_key"},
                    )

                checked_keys.add(user_input[CONF_API_KEY])

            return await self.async_step_geography_finish(user_input)
Esempio n. 14
0
async def test_node_by_id(aresponses):
    """Test getting a node's info by its ID from the cloud API."""
    aresponses.add(
        "www.airvisual.com",
        "/api/v2/node/12345",
        "get",
        aresponses.Response(
            text=load_fixture("node_by_id_response.json"),
            headers={"Content-Type": "application/json"},
            status=200,
        ),
    )

    async with aiohttp.ClientSession() as session:
        cloud_api = CloudAPI(TEST_API_KEY, session=session)
        data = await cloud_api.node.get_by_node_id(TEST_NODE_ID)
        assert data["current"]["tp"] == 2.3
        assert data["current"]["hm"] == 73
        assert data["current"]["p2"] == 35
        assert data["current"]["co"] == 479
Esempio n. 15
0
async def test_aqi_ranking(aresponses):
    """Test getting AQI ranking by city."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/city_ranking",
        "get",
        aresponses.Response(
            text=load_fixture("city_ranking_response.json"),
            headers={"Content-Type": "application/json"},
            status=200,
        ),
    )

    async with aiohttp.ClientSession() as session:
        cloud_api = CloudAPI(TEST_API_KEY, session=session)
        data = await cloud_api.air_quality.ranking()
        assert len(data) == 3
        assert data[0]["city"] == "Portland"
        assert data[0]["state"] == "Oregon"
        assert data[0]["country"] == "USA"
Esempio n. 16
0
async def test_station_by_coordinates(aresponses):
    """Test getting a station by latitude and longitude."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/nearest_station",
        "get",
        aresponses.Response(
            text=load_fixture("station_response.json"),
            headers={"Content-Type": "application/json"},
            status=200,
        ),
    )

    async with aiohttp.ClientSession() as session:
        cloud_api = CloudAPI(TEST_API_KEY, session=session)
        data = await cloud_api.air_quality.nearest_station(
            latitude=TEST_LATITUDE, longitude=TEST_LONGITUDE)
        assert data["city"] == "Beijing"
        assert data["state"] == "Beijing"
        assert data["country"] == "China"
Esempio n. 17
0
async def test_station_by_name(aresponses):
    """Test getting a station by location name."""
    aresponses.add(
        "api.airvisual.com",
        "/v2/station",
        "get",
        aresponses.Response(
            text=load_fixture("station_response.json"),
            headers={"Content-Type": "application/json"},
            status=200,
        ),
    )

    async with aiohttp.ClientSession() as session:
        cloud_api = CloudAPI(TEST_API_KEY, session=session)
        data = await cloud_api.air_quality.station(
            station=TEST_STATION_NAME,
            city=TEST_CITY,
            state=TEST_STATE,
            country=TEST_COUNTRY,
        )
        assert data["city"] == "Beijing"
        assert data["state"] == "Beijing"
        assert data["country"] == "China"
Esempio n. 18
0
async def async_setup_entry(hass, config_entry):
    """Set up AirVisual as config entry."""
    if CONF_API_KEY in config_entry.data:
        _standardize_geography_config_entry(hass, config_entry)

        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 = cloud_api.air_quality.city(
                    config_entry.data[CONF_CITY],
                    config_entry.data[CONF_STATE],
                    config_entry.data[CONF_COUNTRY],
                )
            else:
                api_coro = cloud_api.air_quality.nearest_city(
                    config_entry.data[CONF_LATITUDE],
                    config_entry.data[CONF_LONGITUDE],
                )

            try:
                return await api_coro
            except (InvalidKeyError, KeyExpiredError) as ex:
                raise ConfigEntryAuthFailed from ex
            except AirVisualError as err:
                raise UpdateFailed(
                    f"Error while retrieving data: {err}") from err

        coordinator = DataUpdateCoordinator(
            hass,
            LOGGER,
            name=async_get_geography_id(config_entry.data),
            # We give a placeholder update interval in order to create the coordinator;
            # then, below, we use the coordinator's presence (along with any other
            # coordinators using the same API key) to calculate an actual, leveled
            # update interval:
            update_interval=timedelta(minutes=5),
            update_method=async_update_data,
        )

        # Only geography-based entries have options:
        hass.data[DOMAIN][DATA_LISTENER][
            config_entry.entry_id] = config_entry.add_update_listener(
                async_reload_entry)
    else:
        _standardize_node_pro_config_entry(hass, config_entry)

        async def async_update_data():
            """Get new data from the API."""
            try:
                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

        coordinator = DataUpdateCoordinator(
            hass,
            LOGGER,
            name="Node/Pro data",
            update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL,
            update_method=async_update_data,
        )

    await coordinator.async_config_entry_first_refresh()

    hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator

    # Reassess the interval between 2 server requests
    if CONF_API_KEY in config_entry.data:
        async_sync_geo_coordinator_update_intervals(
            hass, config_entry.data[CONF_API_KEY])

    hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)

    return True
Esempio n. 19
0
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up AirVisual as config entry."""
    if CONF_API_KEY in entry.data:
        _standardize_geography_config_entry(hass, entry)

        websession = aiohttp_client.async_get_clientsession(hass)
        cloud_api = CloudAPI(entry.data[CONF_API_KEY], session=websession)

        async def async_update_data() -> dict[str, Any]:
            """Get new data from the API."""
            if CONF_CITY in entry.data:
                api_coro = cloud_api.air_quality.city(
                    entry.data[CONF_CITY],
                    entry.data[CONF_STATE],
                    entry.data[CONF_COUNTRY],
                )
            else:
                api_coro = cloud_api.air_quality.nearest_city(
                    entry.data[CONF_LATITUDE],
                    entry.data[CONF_LONGITUDE],
                )

            try:
                data = await api_coro
                return cast(dict[str, Any], data)
            except (InvalidKeyError, KeyExpiredError) as ex:
                raise ConfigEntryAuthFailed from ex
            except AirVisualError as err:
                raise UpdateFailed(
                    f"Error while retrieving data: {err}") from err

        coordinator = DataUpdateCoordinator(
            hass,
            LOGGER,
            name=async_get_geography_id(entry.data),
            # We give a placeholder update interval in order to create the coordinator;
            # then, below, we use the coordinator's presence (along with any other
            # coordinators using the same API key) to calculate an actual, leveled
            # update interval:
            update_interval=timedelta(minutes=5),
            update_method=async_update_data,
        )

        # Only geography-based entries have options:
        entry.async_on_unload(entry.add_update_listener(async_reload_entry))
    else:
        # Remove outdated air_quality entities from the entity registry if they exist:
        ent_reg = entity_registry.async_get(hass)
        for entity_entry in [
                e for e in ent_reg.entities.values()
                if e.config_entry_id == entry.entry_id
                and e.entity_id.startswith("air_quality")
        ]:
            LOGGER.debug('Removing deprecated air_quality entity: "%s"',
                         entity_entry.entity_id)
            ent_reg.async_remove(entity_entry.entity_id)

        _standardize_node_pro_config_entry(hass, entry)

        async def async_update_data() -> dict[str, Any]:
            """Get new data from the API."""
            try:
                async with NodeSamba(entry.data[CONF_IP_ADDRESS],
                                     entry.data[CONF_PASSWORD]) as node:
                    data = await node.async_get_latest_measurements()
                    return cast(dict[str, Any], data)
            except NodeProError as err:
                raise UpdateFailed(
                    f"Error while retrieving data: {err}") from err

        coordinator = DataUpdateCoordinator(
            hass,
            LOGGER,
            name="Node/Pro data",
            update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL,
            update_method=async_update_data,
        )

    await coordinator.async_config_entry_first_refresh()
    hass.data.setdefault(DOMAIN, {})
    hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator}

    # Reassess the interval between 2 server requests
    if CONF_API_KEY in entry.data:
        async_sync_geo_coordinator_update_intervals(hass,
                                                    entry.data[CONF_API_KEY])

    hass.config_entries.async_setup_platforms(entry, PLATFORMS)

    return True
Esempio n. 20
0
async def async_setup_entry(hass, config_entry):
    """Set up AirVisual as config entry."""
    if CONF_API_KEY in config_entry.data:
        _standardize_geography_config_entry(hass, config_entry)

        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 = cloud_api.air_quality.city(
                    config_entry.data[CONF_CITY],
                    config_entry.data[CONF_STATE],
                    config_entry.data[CONF_COUNTRY],
                )
            else:
                api_coro = cloud_api.air_quality.nearest_city(
                    config_entry.data[CONF_LATITUDE],
                    config_entry.data[CONF_LONGITUDE],
                )

            try:
                return await api_coro
            except (InvalidKeyError, KeyExpiredError):
                matching_flows = [
                    flow for flow in hass.config_entries.flow.async_progress()
                    if flow["context"]["source"] == SOURCE_REAUTH
                    and flow["context"]["unique_id"] == config_entry.unique_id
                ]

                if not matching_flows:
                    hass.async_create_task(
                        hass.config_entries.flow.async_init(
                            DOMAIN,
                            context={
                                "source": SOURCE_REAUTH,
                                "unique_id": config_entry.unique_id,
                            },
                            data=config_entry.data,
                        ))

                return {}
            except AirVisualError as err:
                raise UpdateFailed(
                    f"Error while retrieving data: {err}") from err

        coordinator = DataUpdateCoordinator(
            hass,
            LOGGER,
            name=async_get_geography_id(config_entry.data),
            # We give a placeholder update interval in order to create the coordinator;
            # then, below, we use the coordinator's presence (along with any other
            # coordinators using the same API key) to calculate an actual, leveled
            # update interval:
            update_interval=timedelta(minutes=5),
            update_method=async_update_data,
        )

        hass.data[DOMAIN][DATA_COORDINATOR][
            config_entry.entry_id] = coordinator
        async_sync_geo_coordinator_update_intervals(
            hass, config_entry.data[CONF_API_KEY])

        # Only geography-based entries have options:
        hass.data[DOMAIN][DATA_LISTENER][
            config_entry.entry_id] = config_entry.add_update_listener(
                async_reload_entry)
    else:
        _standardize_node_pro_config_entry(hass, config_entry)

        async def async_update_data():
            """Get new data from the API."""
            try:
                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

        coordinator = DataUpdateCoordinator(
            hass,
            LOGGER,
            name="Node/Pro data",
            update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL,
            update_method=async_update_data,
        )

        hass.data[DOMAIN][DATA_COORDINATOR][
            config_entry.entry_id] = coordinator

    await coordinator.async_refresh()

    for component in PLATFORMS:
        hass.async_create_task(
            hass.config_entries.async_forward_entry_setup(
                config_entry, component))

    return True
Esempio n. 21
0
async def main() -> None:
    """Create the aiohttp session and run the example."""
    logging.basicConfig(level=logging.INFO)
    async with ClientSession() as session:
        cloud_api = CloudAPI(API_KEY, session=session)

        # Get supported locations (by location):
        try:
            _LOGGER.info(await cloud_api.supported.countries())
            _LOGGER.info(await cloud_api.supported.states("USA"))
            _LOGGER.info(await cloud_api.supported.cities("USA", "Colorado"))
        except AirVisualError as err:
            _LOGGER.error("There was an error: %s", err)

        # Get supported locations (by station):
        try:
            _LOGGER.info(await
                         cloud_api.supported.stations("USA", "Colorado",
                                                      "Denver"))
        except UnauthorizedError as err:
            _LOGGER.error(err)
        except AirVisualError as err:
            _LOGGER.error("There was an error: %s", err)

        # Get data by nearest location (by IP):
        try:
            _LOGGER.info(await cloud_api.air_quality.nearest_city())
        except AirVisualError as err:
            _LOGGER.error("There was an error: %s", err)

        # Get data by nearest location (coordinates or explicit location):
        try:
            _LOGGER.info(await cloud_api.air_quality.nearest_city(
                latitude=39.742599, longitude=-104.9942557))
            _LOGGER.info(await cloud_api.air_quality.city(city="Los Angeles",
                                                          state="California",
                                                          country="USA"))
        except AirVisualError as err:
            _LOGGER.error("There was an error: %s", err)

        # Get data by nearest station (by IP):
        try:
            _LOGGER.info(await cloud_api.air_quality.nearest_station())
        except UnauthorizedError as err:
            _LOGGER.error(err)
        except AirVisualError as err:
            _LOGGER.error("There was an error: %s", err)

        # Get data by nearest station (by coordinates or explicit location):
        try:
            _LOGGER.info(await cloud_api.air_quality.nearest_station(
                latitude=39.742599, longitude=-104.9942557))
            _LOGGER.info(await cloud_api.air_quality.station(
                station="US Embassy in Beijing",
                city="Beijing",
                state="Beijing",
                country="China",
            ))
        except UnauthorizedError as err:
            _LOGGER.error(err)
        except AirVisualError as err:
            _LOGGER.error("There was an error: %s", err)

        # Get data on AQI ranking:
        try:
            _LOGGER.info(await cloud_api.air_quality.ranking())
        except UnauthorizedError as err:
            _LOGGER.error(err)
        except AirVisualError as err:
            _LOGGER.error("There was an error: %s", err)

        # Get a Node/Pro unit via the Cloud API:
        _LOGGER.info(await cloud_api.node.get_by_node_id(NODE_PRO_ID))