diff --git a/README.md b/README.md index e7a7286..f86f547 100644 --- a/README.md +++ b/README.md @@ -17,27 +17,46 @@ Integration supports below sensors of WAQI station: - Rain - Wind speed -It use geolocalized coordinates of station only. +Diffrent stations support diffrent data, "World's Air Quality Index" integration will recognise all parameters (availible in station) according to list of integration's supported sensors. + +There are 2 supported integration methods: + +- using geolocalized coordinates, +- using station ID. + +Notice: waqi.info API supports stations with IDs between 1 and 13837 only. If your station has ID greater than 13837 it won't be able to add using station ID method nor geolocalized coordinates method. waqi.info website shows (on map) much more stations, than are supported by waqi.info API. All others stations are integrated with WAQI map from others websites. In the future waqi.info API would be developed, and there would be more supported stations, you can always check if your station is supported. You just need to copy below link, paste to the URL of web browser, change number of interested station, and paste your token insted {{token}} + +`https://api.waqi.info/feed/@13837/?token={{token}}` + +Web browser will receive some data, if station is supported or "Unknown ID" message, if it doesn't. # Installation -Copy worlds_air_quality_index folder into /config/custom_components of Home Assistant instance. -You can also use HACS to install this repository. +Use HACS to install this repository. +You can also copy worlds_air_quality_index folder into /config/custom_components of Home Assistant instance, then restart HA. # Adding Integration -To add integration use "Add Integration" of Home Assistant UI, and choose "World's Air Quality Index". -In popup window put: +To add integration use "Add Integration" button in section Settings->Devices&Services section, and choose "World's Air Quality Index". +In popup window choose method of station adding: -- your waqi.info account token (required) -- your own name of station (optional) -- latitude of WAQI station (optional) -- longitude of WAQI station (optional) +- using geographic localization, +- using station ID. -To get WAQI token you need to sign up on waqi.info. -If you won't put geolocalized coordinates of station, it will take your home coordinates, and it will find the clostest station. -If you won't put your own name it will take name of found station. +In case of geographic localization, there will be shown next window, where you need to put: -You can add more than 1 station. +- your waqi.info account token (required), +- latitude of WAQI station (required), +- longitude of WAQI station (required), +- your own name of station (optional). + +In case of station ID, there will be shown next window, where you need to put: + +- your waqi.info account token (required), +- ID of WAQI station (required), +- your own name of station (optional). -Diffrent stations support diffrent data, "World's Air Quality Index" integration will recognise all parameters according to list of integration's supported sensors. +To get WAQI token you need to sign up [here](https://aqicn.org/data-platform/token/). +As a default your home coordinates (set in HA) are putten in latitude and longitude fields in geographic localization method. This integration will find the clostest station, what is supported by waqi.info API. +If you won't put your own name it will take name of found station. +You can add more than 1 station. diff --git a/custom_components/worlds_air_quality_index/config_flow.py b/custom_components/worlds_air_quality_index/config_flow.py index daefe29..d34bf1b 100644 --- a/custom_components/worlds_air_quality_index/config_flow.py +++ b/custom_components/worlds_air_quality_index/config_flow.py @@ -15,27 +15,23 @@ CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, - CONF_TOKEN + CONF_TOKEN, + CONF_LOCATION, + CONF_METHOD, + CONF_ID ) from .const import ( DOMAIN, - DEFAULT_NAME -) - -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_LATITUDE): cv.string, - vol.Optional(CONF_LONGITUDE): cv.string, - } + DEFAULT_NAME, + GEOGRAPHIC_LOCALIZATION, + STATION_ID ) class WorldsAirQualityIndexConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for worlds_air_quality_index integration.""" - VERSION = 1 + VERSION = 2 async def async_step_import(self, config: dict[str, Any]) -> FlowResult: """Import a configuration from config.yaml.""" @@ -45,47 +41,151 @@ async def async_step_import(self, config: dict[str, Any]) -> FlowResult: config[CONF_NAME] = name return await self.async_step_user(user_input=config) - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: """Handle the initial step.""" + data_schema = vol.Schema( + { + vol.Required(CONF_METHOD, default=GEOGRAPHIC_LOCALIZATION): vol.In( + ( + GEOGRAPHIC_LOCALIZATION, + STATION_ID + ) + ) + } + ) + + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=data_schema, + ) + + if user_input[CONF_METHOD] == GEOGRAPHIC_LOCALIZATION: + return await self.async_step_geographic_localization() + return await self.async_step_station_id() + + async def async_step_geographic_localization(self, user_input=None) -> FlowResult: + """Handle the geographic localization step.""" errors = {} - if user_input: + data_schema = vol.Schema( + { + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_LATITUDE, default=self.hass.config.latitude): cv.latitude, + vol.Required(CONF_LONGITUDE, default=self.hass.config.longitude): cv.longitude, + vol.Optional(CONF_NAME): cv.string + } + ) + if user_input: token = user_input[CONF_TOKEN] - latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) - longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) - requester = WaqiDataRequester(latitude, longitude, token) + latitude = user_input[CONF_LATITUDE] + longitude = user_input[CONF_LONGITUDE] + method = CONF_LOCATION + requester = WaqiDataRequester(latitude, longitude, token, None, method) await self.hass.async_add_executor_job(requester.update) - testData = requester.GetData() + validateData = requester.GetData() + if validateData: + if validateData["status"] == "ok": + if "status" in validateData["data"]: + if validateData["data"]["status"] == "error": + if validateData["data"]["msg"] == "Unknown ID": + errors["base"] = "unknow_station_id" + else: + errors["base"] = "server_error" + elif validateData["status"] == "error": + if validateData["data"] == "Invalid key": + errors["base"] = "invalid_token" + else: + errors["base"] = "server_error" + else: + errors["base"] = "server_error" + else: + errors["base"] = "server_not_available" + stationName = requester.GetStationName() name = user_input.get(CONF_NAME, stationName) - if testData is None: - errors["base"] = "invalid_token" - elif stationName is None: - errors["base"] = "invalid_station_name" - else: + if not errors: await self.async_set_unique_id(name) self._abort_if_unique_id_configured() return self.async_create_entry( title=name, data={ - CONF_NAME: name, CONF_TOKEN: token, CONF_LATITUDE: latitude, CONF_LONGITUDE: longitude, + CONF_NAME: name, + CONF_METHOD: method, }, ) return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, + step_id="geographic_localization", + data_schema=data_schema, errors=errors, ) - + async def async_step_station_id(self, user_input=None) -> FlowResult: + errors = {} + + data_schema = vol.Schema( + { + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_ID): cv.string, + vol.Optional(CONF_NAME): cv.string + } + ) + + if user_input: + + token = user_input[CONF_TOKEN] + id = user_input[CONF_ID] + method = CONF_ID + requester = WaqiDataRequester(None, None, token, id, method) + await self.hass.async_add_executor_job(requester.update) + + validateData = requester.GetData() + if validateData: + if validateData["status"] == "ok": + if "status" in validateData["data"]: + if validateData["data"]["status"] == "error": + if validateData["data"]["msg"] == "Unknown ID": + errors["base"] = "unknow_station_id" + else: + errors["base"] = "server_error" + elif validateData["status"] == "error": + if validateData["data"] == "Invalid key": + errors["base"] = "invalid_token" + else: + errors["base"] = "server_error" + else: + errors["base"] = "server_error" + else: + errors["base"] = "server_not_available" + + stationName = requester.GetStationName() + name = user_input.get(CONF_NAME, stationName) + + if not errors: + await self.async_set_unique_id(name) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=name, + data={ + CONF_TOKEN: token, + CONF_ID: id, + CONF_NAME: name, + CONF_METHOD: method, + }, + ) + + return self.async_show_form( + step_id="station_id", + data_schema=data_schema, + errors=errors, + ) diff --git a/custom_components/worlds_air_quality_index/const.py b/custom_components/worlds_air_quality_index/const.py index d71d4f4..25a857f 100644 --- a/custom_components/worlds_air_quality_index/const.py +++ b/custom_components/worlds_air_quality_index/const.py @@ -15,10 +15,13 @@ DOMAIN = "worlds_air_quality_index" PLATFORMS = [Platform.SENSOR] -SW_VERSION = "0.2.0" +SW_VERSION = "0.3.0" SCAN_INTERVAL = timedelta(minutes=30) +DISCOVERY_TYPE = "discovery_type" +GEOGRAPHIC_LOCALIZATION = "Geographic localization" +STATION_ID = "Station ID" DEFAULT_NAME = 'waqi1' SENSORS = { diff --git a/custom_components/worlds_air_quality_index/manifest.json b/custom_components/worlds_air_quality_index/manifest.json index dcf0851..80ef18d 100644 --- a/custom_components/worlds_air_quality_index/manifest.json +++ b/custom_components/worlds_air_quality_index/manifest.json @@ -11,5 +11,5 @@ "dependencies": [], "codeowners": ["@pawkakol1"], "iot_class": "cloud_polling", - "version": "0.2.0" + "version": "0.3.0" } diff --git a/custom_components/worlds_air_quality_index/sensor.py b/custom_components/worlds_air_quality_index/sensor.py index 8505f7d..b38117d 100644 --- a/custom_components/worlds_air_quality_index/sensor.py +++ b/custom_components/worlds_air_quality_index/sensor.py @@ -24,7 +24,9 @@ CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, - CONF_TOKEN + CONF_TOKEN, + CONF_ID, + CONF_METHOD ) from .const import ( @@ -67,7 +69,6 @@ async def async_setup_platform( ) ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -75,21 +76,36 @@ async def async_setup_entry( _LOGGER.debug("config token:") _LOGGER.debug(entry.data[CONF_TOKEN]) - _LOGGER.debug(entry.data[CONF_LATITUDE]) - _LOGGER.debug(entry.data[CONF_LONGITUDE]) + _LOGGER.debug("config method:") + _LOGGER.debug(entry.data[CONF_METHOD]) + _LOGGER.debug("config name:") + _LOGGER.debug(entry.data[CONF_NAME]) name = entry.data[CONF_NAME] token = entry.data[CONF_TOKEN] - latitude = entry.data[CONF_LATITUDE] - longitude = entry.data[CONF_LONGITUDE] - requester = WaqiDataRequester(latitude, longitude, token) + method = entry.data[CONF_METHOD] + + if method == CONF_ID: + _LOGGER.debug("config ID:") + _LOGGER.debug(entry.data[CONF_ID]) + id = entry.data[CONF_ID] + requester = WaqiDataRequester(None, None, token, id, method) + else: + _LOGGER.debug("config latitude:") + _LOGGER.debug(entry.data[CONF_LATITUDE]) + _LOGGER.debug("config longitude:") + _LOGGER.debug(entry.data[CONF_LONGITUDE]) + latitude = entry.data[CONF_LATITUDE] + longitude = entry.data[CONF_LONGITUDE] + requester = WaqiDataRequester(latitude, longitude, token, None, method) + await hass.async_add_executor_job(requester.update) scannedData = requester.GetData() scannedData = scannedData["data"]["iaqi"] entities = [] - #entities.append(WorldsAirQualityIndexAqiSensor(requester)) + for res in SENSORS: if res == "aqi" or res in scannedData: entities.append(WorldsAirQualityIndexSensor(res, requester)) diff --git a/custom_components/worlds_air_quality_index/strings.json b/custom_components/worlds_air_quality_index/strings.json index 0f48c60..4a334af 100644 --- a/custom_components/worlds_air_quality_index/strings.json +++ b/custom_components/worlds_air_quality_index/strings.json @@ -1,19 +1,41 @@ { "config": { + "error": { + "invalid_token": "Invalid token", + "unknow_station_id": "WAQI API doesn't support this station ID", + "server_error": "Server error", + "server_not_available": "Server is not available", + "invalid_station_name": "Invalid station name" + }, "step": { "user": { + "title": "Choose station adding method WAQI", + "description": "Choose station adding method WAQI, what you preffered: using geographic localization or using station ID", "data": { - "name": "Name of service", - "token": "Token key of World's Air Quality Index account", - "latitude": "Latitude of station or site", - "longitude": "Longitude of station or site", + "geographic_localization": "Geographic localization", + "station_id": "Station ID", "scan_interval": "Scan interval of station updating" } + }, + "geographic_localization": { + "title": "Add WAQI station using grographic localization", + "description": "Fill your WAQI token, latitude and longitude of your station or home, and optionally custom name for service instace. You can find WAQI token here https://aqicn.org/data-platform/token/", + "data": { + "token": "Token key of World's Air Quality Index account", + "latitude": "Latitude of station", + "longitude": "Longitude of station", + "name": "Name of service" + } + }, + "station_id": { + "title": "Add WAQI station using ID", + "description": "Fill your WAQI token, id of your station (without @ prefix) and optionally custom name for service instace. You can find WAQI token here https://aqicn.org/data-platform/token/", + "data": { + "token": "Token key of World's Air Quality Index account", + "id": "Station ID", + "name": "Name of service" + } } - }, - "error": { - "invalid_token": "Invalid token", - "invalid_station_name": "Invalid station name" } } } diff --git a/custom_components/worlds_air_quality_index/translations/en.json b/custom_components/worlds_air_quality_index/translations/en.json index 7a64ef1..4a334af 100644 --- a/custom_components/worlds_air_quality_index/translations/en.json +++ b/custom_components/worlds_air_quality_index/translations/en.json @@ -2,17 +2,39 @@ "config": { "error": { "invalid_token": "Invalid token", + "unknow_station_id": "WAQI API doesn't support this station ID", + "server_error": "Server error", + "server_not_available": "Server is not available", "invalid_station_name": "Invalid station name" }, "step": { "user": { + "title": "Choose station adding method WAQI", + "description": "Choose station adding method WAQI, what you preffered: using geographic localization or using station ID", "data": { - "name": "Name of service", - "token": "Token key of World's Air Quality Index account", - "latitude": "Latitude of station or site", - "longitude": "Longitude of station or site", + "geographic_localization": "Geographic localization", + "station_id": "Station ID", "scan_interval": "Scan interval of station updating" } + }, + "geographic_localization": { + "title": "Add WAQI station using grographic localization", + "description": "Fill your WAQI token, latitude and longitude of your station or home, and optionally custom name for service instace. You can find WAQI token here https://aqicn.org/data-platform/token/", + "data": { + "token": "Token key of World's Air Quality Index account", + "latitude": "Latitude of station", + "longitude": "Longitude of station", + "name": "Name of service" + } + }, + "station_id": { + "title": "Add WAQI station using ID", + "description": "Fill your WAQI token, id of your station (without @ prefix) and optionally custom name for service instace. You can find WAQI token here https://aqicn.org/data-platform/token/", + "data": { + "token": "Token key of World's Air Quality Index account", + "id": "Station ID", + "name": "Name of service" + } } } } diff --git a/custom_components/worlds_air_quality_index/translations/pl.json b/custom_components/worlds_air_quality_index/translations/pl.json index 9ac81df..0f82cc5 100644 --- a/custom_components/worlds_air_quality_index/translations/pl.json +++ b/custom_components/worlds_air_quality_index/translations/pl.json @@ -2,16 +2,38 @@ "config": { "error": { "invalid_token": "Niewłaściwy token", + "unknow_station_id": "WAQI API nie obsługuje tego ID stacji", + "server_error": "Błąd serwera", + "server_not_available": "Serwer jest niedostępny", "invalid_station_name": "Niewłaściwa nazwa stacji" }, "step": { "user": { + "title": "Wybierz metodę dodawania stacji WAQI", + "description": "Wybierz metodę dodawania stacji WAQI, którą preferujesz: wykorzystując lokalizację geograficzną lub wykorzystując ID stacji", + "data": { + "geographic_localization": "Lokalizacja geograficzna", + "station_id": "ID stacji", + "scan_interval": "Częstotliwość aktualizacji danych stacji" + } + }, + "geographic_localization": { + "title": "Dodaj stację WAQI wykorzystująć lokalizację geograficzną", + "description": "Wypełnij pola swoim tokenem WAQI, szerokością i długością geograficzną swojej stacji lub domu, a także opcjonalnie własną nazwą serwisu. Token WAQI można znaleźć tutaj https://aqicn.org/data-platform/token/", "data": { - "name": "Nazwa serwisu", "token": "Klucz tokena konta World's Air Quality Index", "latitude": "Szerokość geograficzna stacji lub obiektu", "longitude": "Długość geograficzna stacji lub obiektu", - "scan_interval": "Częstotliwość aktualizacji danych stacji" + "name": "Nazwa serwisu" + } + }, + "station_id": { + "title": "Dodaj stację WAQI wykorzystując ID", + "description": "Wypełnij pola swoim tokenem WAQI, ID swojej stacji, a także opcjonalnie własną nazwą serwisu. Token WAQI można znaleźć tutaj https://aqicn.org/data-platform/token/", + "data": { + "token": "Klucz tokena konta World's Air Quality Index", + "id": "ID stacji", + "name": "Nazwa serwisu" } } } diff --git a/custom_components/worlds_air_quality_index/waqi_api.py b/custom_components/worlds_air_quality_index/waqi_api.py index eab4730..1cc88a9 100644 --- a/custom_components/worlds_air_quality_index/waqi_api.py +++ b/custom_components/worlds_air_quality_index/waqi_api.py @@ -2,16 +2,22 @@ import requests import logging from homeassistant.util import Throttle +from homeassistant.const import ( + CONF_ID, + CONF_LOCATION +) from .const import SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) class WaqiDataRequester(object): - def __init__(self, lat, lng, token): + def __init__(self, lat, lng, token, idx, method): self._lat = lat self._lng = lng self._token = token + self._idx = idx + self._method = method self._data = None self._stationName = None self._stationIdx = None @@ -20,18 +26,42 @@ def __init__(self, lat, lng, token): @Throttle(SCAN_INTERVAL) def update(self): _LOGGER.debug("Updating WAQI sensors") - try: - _dat = requests.get(f"https://api.waqi.info/feed/geo:{self._lat};{self._lng}/?token={self._token}").text - self._data = json.loads(_dat) - self._stationName = self._data["data"]["city"]["name"] - self._stationName = self._stationName.replace(", ", "_").replace(" ", "_").replace("(", "").replace(")","").lower() - self._stationIdx = self._data["data"]["idx"] - self._updateLastTime = self._data["data"]["time"]["iso"] + if self._method == CONF_LOCATION: + _dat = requests.get(f"https://api.waqi.info/feed/geo:{self._lat};{self._lng}/?token={self._token}").text + elif self._method == CONF_ID: + _dat = requests.get(f"https://api.waqi.info/feed/@{self._idx}/?token={self._token}").text + else: + _LOGGER.debug("No choosen method") + + if _dat: + self._data = json.loads(_dat) + if self._data: + if "data" in self._data: + if "idx" in self._data["data"]: + if self._method == CONF_LOCATION: + self._stationIdx = self._data["data"]["idx"] + elif self._method == CONF_ID: + self._stationIdx = self._idx + + if "city" in self._data["data"]: + if "name" in self._data["data"]["city"]: + self._stationName = self._data["data"]["city"]["name"] + + if self._stationName: + self._stationName = self._stationName.replace(", ", "_").replace(" ", "_").replace("(", "").replace(")","").lower() + else: + self._stationName = "UnknownName_" + self._stationIdx + + if "time" in self._data["data"]: + if "iso" in self._data["data"]["time"]: + self._updateLastTime = self._data["data"]["time"]["iso"] + except requests.exceptions.RequestException as exc: _LOGGER.error("Error occurred while fetching data: %r", exc) self._data = None self._stationName = None + self._stationIdx = None return False def GetData(self):