diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..af378d9 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +.github/* @JohNan \ No newline at end of file diff --git a/.github/delete-merged-branch-config.yml b/.github/delete-merged-branch-config.yml new file mode 100644 index 0000000..6c8c703 --- /dev/null +++ b/.github/delete-merged-branch-config.yml @@ -0,0 +1,3 @@ +exclude: + - master + - next \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..3fc8ef7 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,37 @@ +name-template: 'v$RESOLVED_VERSION 🌈' +tag-template: 'v$RESOLVED_VERSION' +change-template: '- #$NUMBER $TITLE @$AUTHOR' +sort-direction: ascending +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + + - title: '🧰 Maintenance' + label: 'chore' + +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + + $CHANGES + + ## Thank you so much for helping out to keep this integration awesome + $CONTRIBUTORS \ No newline at end of file diff --git a/.github/workflows/cron.yaml b/.github/workflows/cron.yaml new file mode 100644 index 0000000..9475676 --- /dev/null +++ b/.github/workflows/cron.yaml @@ -0,0 +1,28 @@ +name: "Cron actions 16:30 every 4 days" +on: + schedule: + - cron: '30 16 */4 * *' + +jobs: + validate-hassfest: + name: With hassfest + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Hassfest validation + uses: home-assistant/actions/hassfest@master + + validate-hacs: + name: With HACS action + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: HACS Validation + uses: hacs/action@main + with: + category: integration + comment: False diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml new file mode 100644 index 0000000..c906184 --- /dev/null +++ b/.github/workflows/release-drafter.yaml @@ -0,0 +1,18 @@ +name: Release Drafter + +on: + push: + branches: + - master + +jobs: + update_release_draft: + name: Update release draft + runs-on: ubuntu-latest + steps: + - name: Create Release + uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..40efe0f --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,35 @@ +name: Release + +on: + release: + types: [published] + +jobs: + release_zip_file: + name: Prepare release asset + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Get version + id: version + uses: home-assistant/actions/helpers/version@master + + - name: "Set version number" + run: | + python3 ${{ github.workspace }}/manage/update_manifest.py --version ${{ steps.version.outputs.version }} + + - name: Create zip + run: | + cd custom_components/pollenprognos + zip pollenprognos.zip -r ./ + + - name: Upload zip to release + uses: svenstaro/upload-release-action@v1-release + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ./custom_components/pollenprognos/pollenprognos.zip + asset_name: pollenprognos.zip + tag: ${{ github.ref }} + overwrite: true \ No newline at end of file diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 0000000..9cd31b9 --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,33 @@ +name: "Validate" + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + validate-hassfest: + name: With hassfest + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: Hassfest validation + uses: home-assistant/actions/hassfest@master + + validate-hacs: + name: With HACS action + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + + - name: HACS Validation + uses: hacs/action@main + with: + category: integration + comment: True \ No newline at end of file diff --git a/.github/workflows/validate_pr.yaml b/.github/workflows/validate_pr.yaml new file mode 100644 index 0000000..1fba60a --- /dev/null +++ b/.github/workflows/validate_pr.yaml @@ -0,0 +1,19 @@ +name: "Validate" + +on: + pull_request_target: + types: [opened, labeled, unlabeled, synchronize] + +jobs: + validate-labels: + name: With PR label action + runs-on: ubuntu-latest + steps: + - name: PR Label Validation + uses: jesusvasquez333/verify-pr-label-action@v1.4.0 + with: + github-token: '${{ secrets.GITHUB_TOKEN }}' + valid-labels: 'bug, enhancement' + invalid-labels: 'help wanted, invalid, question' + pull-request-number: '${{ github.event.pull_request.number }}' + disable-reviews: true \ No newline at end of file diff --git a/README.md b/README.md index b415471..da1151f 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,20 @@ -# Home Assistant PollennivĂ„ +# Home Assistant Pollenprognos Support for getting current pollen levels from Pollenkollen.se -Visit https://pollenkoll.se/pollenprognos/ to find available cities -Visit https://pollenkoll.se/pollenprognos-ostersund/ to find available allergens -Available states: +### Install with HACS (recommended) +Add the url to the repository as a custom integration. -``` -STATES = { - "i.h.": 0, - "L": 1, - "L-M": 2, - "M": 3, - "M-H": 4, - "H": 5, - "H-H+": 6 -} -``` - -Place the folder `pollenniva` in `/custom_components` -Add configuration to your `configuration.yaml` - -This will create sensors named `senson.pollenniva_CITY_ALLERGEN_day_[0-3]` and the state will be the current level of that allergen. - -Example configuration - -``` -sensor: - - platform: pollenniva - scan_interval: 14400 # (default=14400 seconds (4 hours), optional) - state_as_string: false # (default=false, optional, show states as strings as per STATES below) - sensors: - - city: Stockholm - days_to_track: 4 # (default=4, values 1-4, optional) - allergens: - - GrĂ€s - - Hassel - - city: Östersund - allergens: - - Hassel -``` +### Configure +This integration can be configured via the Home Assistant frontend. +- Go to **Configuration** -> **Integrations**. +- Click on the `+` in the bottom right corner to add a new integration. +- Search and select the **Pollenprognos** integration form the list. +- Follow the instruction on screen to add the sensors. ### Custom card for Lovelace Custom card for Lovelace can be found here: https://github.com/isabellaalstrom/pollenkoll-card Pollenkoll Lovelace Card - -### Automatic updates - -For update check of this sensor, add the following to your configuration.yaml. For more information, see [custom_updater](https://github.com/custom-components/custom_updater) - -Example configuration -``` -custom_updater: - track: - - components - component_urls: - - https://raw.githubusercontent.com/JohNan/home-assistant-pollenkoll/master/custom_updater.json -``` diff --git a/custom_component/pollenprognos/__init__.py b/custom_component/pollenprognos/__init__.py new file mode 100644 index 0000000..13af822 --- /dev/null +++ b/custom_component/pollenprognos/__init__.py @@ -0,0 +1,95 @@ +"""Pollenprognos Custom Component.""" +import asyncio +import logging +from datetime import timedelta, datetime + +from .api import PollenApi +from .const import DOMAIN, PLATFORMS +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, Config +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import UpdateFailed, DataUpdateCoordinator + +SCAN_INTERVAL = timedelta(hours=4) + +_LOGGER: logging.Logger = logging.getLogger(__package__) + + +async def async_setup(hass: HomeAssistant, config: Config): + """Set up this integration using YAML is not supported.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up this integration using UI.""" + if hass.data.get(DOMAIN) is None: + hass.data.setdefault(DOMAIN, {}) + + session = async_get_clientsession(hass) + client = PollenApi(session) + + coordinator = PollenprognosDataUpdateCoordinator(hass, client=client) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = coordinator + + for platform in PLATFORMS: + if entry.options.get(platform, True): + coordinator.platforms.append(platform) + hass.async_add_job( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + entry.add_update_listener(async_reload_entry) + return True + + +class PollenprognosDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the API.""" + + def __init__( + self, hass: HomeAssistant, client: PollenApi + ) -> None: + """Initialize.""" + self.api = client + self.platforms = [] + self.last_updated = None + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + + async def _async_update_data(self): + """Update data via library.""" + try: + data = await self.api.async_get_data() + self.last_updated = datetime.now() + return data + except Exception as exception: + raise UpdateFailed() from exception + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle removal of an entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + unloaded = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + if platform in coordinator.platforms + ] + ) + ) + if unloaded: + hass.data[DOMAIN].pop(entry.entry_id) + + return unloaded + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload config entry.""" + await async_unload_entry(hass, entry) + await async_setup_entry(hass, entry) \ No newline at end of file diff --git a/custom_component/pollenprognos/api.py b/custom_component/pollenprognos/api.py new file mode 100644 index 0000000..08e285e --- /dev/null +++ b/custom_component/pollenprognos/api.py @@ -0,0 +1,66 @@ +import asyncio +import logging +import socket + +import aiohttp +import async_timeout + +TIMEOUT = 10 + +_LOGGER: logging.Logger = logging.getLogger(__package__) + +HEADERS = { + "Content-type": "application/json; charset=UTF-8", + "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/PPR1.180610.011)", +} + + +class PollenApi: + def __init__(self, session: aiohttp.ClientSession) -> None: + self._session = session + + async def async_get_data(self) -> dict: + """Get data from the API.""" + url = "https://pollenkoll.se/wp-json/pollenkoll/v1/app-complete?secret=830842cf-4d86-412d-a343-16edc27f5c75&platform=android&version=3" + return await self.api_wrapper("get", url) + + async def api_wrapper( + self, method: str, url: str, data: dict = {}, headers: dict = {} + ) -> dict: + """Get information from the API.""" + try: + async with async_timeout.timeout(TIMEOUT, loop=asyncio.get_event_loop()): + if method == "get": + response = await self._session.get(url, headers=headers) + return await response.json() + + elif method == "put": + await self._session.put(url, headers=headers, json=data) + + elif method == "patch": + await self._session.patch(url, headers=headers, json=data) + + elif method == "post": + await self._session.post(url, headers=headers, json=data) + + except asyncio.TimeoutError as exception: + _LOGGER.error( + "Timeout error fetching information from %s - %s", + url, + exception, + ) + + except (KeyError, TypeError) as exception: + _LOGGER.error( + "Error parsing information from %s - %s", + url, + exception, + ) + except (aiohttp.ClientError, socket.gaierror) as exception: + _LOGGER.error( + "Error fetching information from %s - %s", + url, + exception, + ) + except Exception as exception: # pylint: disable=broad-except + _LOGGER.error("Something really wrong happened! - %s", exception) \ No newline at end of file diff --git a/custom_component/pollenprognos/config_flow.py b/custom_component/pollenprognos/config_flow.py new file mode 100644 index 0000000..21a2a0e --- /dev/null +++ b/custom_component/pollenprognos/config_flow.py @@ -0,0 +1,86 @@ +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +from .api import PollenApi +from homeassistant import config_entries +from .const import PLATFORMS, DOMAIN, CONF_ALLERGENS, CONF_NAME, CONF_CITY +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_create_clientsession + + +class PollenprognosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Blueprint.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize.""" + self.data = None + self.task_fetch_cities = None + self._init_info = {} + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if not self.task_fetch_cities: + if not self.task_fetch_cities: + self.task_fetch_cities = self.hass.async_create_task(self._async_task_fetch_cities()) + return self.async_show_progress( + step_id="user", + progress_action="fetch_cities", + ) + + # noinspection PyBroadException + try: + await self.task_fetch_cities + except Exception: # pylint: disable=broad-except + return self.async_show_progress_done(next_step_id="install_failed") + + return self.async_show_progress_done(next_step_id="select_city") + + async def async_step_select_city(self, user_input=None): + if user_input is not None: + self._init_info[CONF_CITY] = user_input[CONF_CITY] + self._init_info[CONF_NAME] = next(item for item in self.data.get('cities', []).get('cities', []) if + item["id"] == self._init_info[CONF_CITY])['name'] + return await self.async_step_select_pollen() + + cities = {city['id']: city['name'] for city in self.data.get('cities', []).get('cities', []) } + return self.async_show_form( + step_id="select_city", + data_schema=vol.Schema( + { + vol.Required(CONF_CITY, default=list(cities.keys())): vol.In(cities) + } + ) + ) + + async def async_step_select_pollen(self, user_input=None): + if user_input is not None: + self._init_info[CONF_ALLERGENS] = user_input[CONF_ALLERGENS] + await self.async_set_unique_id(f"{self._init_info[CONF_CITY]}-{self._init_info[CONF_NAME]}") + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self._init_info[CONF_NAME], data=self._init_info + ) + + pollen = {pollen['type_code']: pollen['type'] for pollen in self.data.get('pollen_types', [])} + return self.async_show_form( + step_id="select_pollen", + data_schema=vol.Schema( + { + vol.Required(CONF_ALLERGENS, default=list(pollen.keys())): cv.multi_select(pollen) + } + ) + ) + + async def _async_task_fetch_cities(self): + try: + session = async_create_clientsession(self.hass) + client = PollenApi(session) + self.data = await client.async_get_data() + finally: + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + ) \ No newline at end of file diff --git a/custom_component/pollenprognos/const.py b/custom_component/pollenprognos/const.py new file mode 100644 index 0000000..83541fc --- /dev/null +++ b/custom_component/pollenprognos/const.py @@ -0,0 +1,38 @@ +"""Pollenprognos Custom Update Version.""" +NAME = "Pollenprognos" +VERSION = '1.0.0' +DOMAIN = 'pollenprognos' + +# Platforms +SENSOR = "sensor" +PLATFORMS = [SENSOR] + +CONF_CITY = 'conf_city' +CONF_ALLERGENS = 'conf_allergens' +CONF_NAME = 'conf_name' + +STATES = { + "i.h.": 0, + "L": 1, + "L-M": 2, + "M": 3, + "M-H": 4, + "H": 5, + "H-H+": 6, + "H+": 7 +} + +SENSOR_ICONS = { + 'al': 'mdi:leaf', + 'alm': 'mdi:leaf', + 'ambrosia': 'mdi:leaf', + 'asp': 'mdi:leaf', + 'bok': 'mdi:leaf', + 'bjork': 'mdi:leaf', + 'ek': 'mdi:leaf', + 'grabo': 'mdi:flower', + 'gras': 'mdi:flower', + 'hassel': 'mdi:leaf', + 'salg_vide': 'mdi:leaf', + 'default': 'mdi:leaf' +} diff --git a/custom_component/pollenprognos/entity.py b/custom_component/pollenprognos/entity.py new file mode 100644 index 0000000..2247dc7 --- /dev/null +++ b/custom_component/pollenprognos/entity.py @@ -0,0 +1,32 @@ +"""PollenEntity class""" +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, NAME, VERSION, CONF_NAME + + +class PollenEntity(CoordinatorEntity): + def __init__(self, coordinator, config_entry): + super().__init__(coordinator) + self.config_entry = config_entry + + @property + def unique_id(self): + """Return a unique ID to use for this entity.""" + return f"{self.config_entry.entry_id}-{self.name}" + + @property + def device_info(self): + return { + "identifiers": {(DOMAIN, self.config_entry.data[CONF_NAME])}, + "name": f"{NAME} {self.config_entry.data[CONF_NAME]}", + "model": VERSION, + "manufacturer": "Pollenkollen.se", + } + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + "update_success": self.coordinator.last_update_success, + "last_updated": self.coordinator.last_updated.strftime("%Y-%m-%d %H:%M:%S") if self.coordinator.last_updated else None + } \ No newline at end of file diff --git a/custom_component/pollenprognos/manifest.json b/custom_component/pollenprognos/manifest.json new file mode 100644 index 0000000..e25f466 --- /dev/null +++ b/custom_component/pollenprognos/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "pollenprognos", + "name": "Pollenprognos", + "documentation": "https://github.com/JohNan/home-assistant-pollenkoll", + "dependencies": [], + "codeowners": ["@JohNan"], + "config_flow": true, + "version": "v0.0.0", + "requirements": [] +} \ No newline at end of file diff --git a/custom_component/pollenprognos/sensor.py b/custom_component/pollenprognos/sensor.py new file mode 100644 index 0000000..1084d82 --- /dev/null +++ b/custom_component/pollenprognos/sensor.py @@ -0,0 +1,73 @@ +""" +Support for getting current pollen levels from Pollenkollen.se +""" + +import logging +import json + +from collections import namedtuple +from datetime import timedelta +from typing import Any + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import ENTITY_ID_FORMAT + +from dateutil import parser +from datetime import datetime +from .const import VERSION, DOMAIN, SENSOR_ICONS, CONF_CITY, CONF_ALLERGENS, CONF_NAME +from .entity import PollenEntity + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_devices): + """Setup sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + if not coordinator.data: + return False + + city = next(item for item in coordinator.data.get('cities', []).get('cities', []) if item["id"] == entry.data[CONF_CITY]) + allergens = {pollen['type_code']: pollen['type'] for pollen in city.get('pollen', []) if pollen['type_code'] in entry.data[CONF_ALLERGENS]} + async_add_devices([ + PollenSensor(name, allergen, coordinator, entry) + for (allergen, name) in allergens.items() + ]) + + return True + + +class PollenSensor(PollenEntity): + """Representation of a Pollen sensor.""" + + def __init__(self, name, allergen_type, coordinator, config_entry): + super().__init__(coordinator, config_entry) + self._allergen_type = allergen_type + self._name = name + self.entity_id = ENTITY_ID_FORMAT.format(f"{self.config_entry.data[CONF_NAME]}_{self._allergen_type}") + + @property + def _allergen(self): + city = next(item for item in self.coordinator.data.get('cities', []).get('cities', []) if + item["id"] == self.config_entry.data[CONF_CITY]) + return next(item for item in city.get('pollen', []) if item['type_code'] == self._allergen_type) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + today = next(item for item in self._allergen.get('days', []) if item['day'] == 0) + return today.get('level', 'n/a') + + @property + def state_attributes(self): + return {day['day_name']: day['level'] for day in self._allergen.get('days', []) if day['day'] != 0} + + @property + def icon(self): + """ Return the icon for the frontend.""" + return SENSOR_ICONS.get(self._allergen_type, 'default') diff --git a/custom_component/pollenprognos/strings.json b/custom_component/pollenprognos/strings.json new file mode 100644 index 0000000..6139afd --- /dev/null +++ b/custom_component/pollenprognos/strings.json @@ -0,0 +1,31 @@ +{ + "title": "Pollenprognos", + "config": { + "step": { + "user": { + "title": "Pollenprognos configuration", + "description": "Retrieves pollen forecasts" + }, + "select_city": { + "title": "Pollenprognos - City", + "description": "Choose which city you want to monitor", + "data": { + "conf_city": "Select city" + } + }, + "select_pollen": { + "title": "Pollenprognos - Pollen", + "description": "Choose which pollen you want to monitor", + "data": { + "conf_pollen": "Select pollen" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "progress": { + "fetch_cities": "Retrieves pollen forecasts" + } + } +} \ No newline at end of file diff --git a/custom_component/pollenprognos/translations/en.json b/custom_component/pollenprognos/translations/en.json new file mode 100644 index 0000000..6139afd --- /dev/null +++ b/custom_component/pollenprognos/translations/en.json @@ -0,0 +1,31 @@ +{ + "title": "Pollenprognos", + "config": { + "step": { + "user": { + "title": "Pollenprognos configuration", + "description": "Retrieves pollen forecasts" + }, + "select_city": { + "title": "Pollenprognos - City", + "description": "Choose which city you want to monitor", + "data": { + "conf_city": "Select city" + } + }, + "select_pollen": { + "title": "Pollenprognos - Pollen", + "description": "Choose which pollen you want to monitor", + "data": { + "conf_pollen": "Select pollen" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "progress": { + "fetch_cities": "Retrieves pollen forecasts" + } + } +} \ No newline at end of file diff --git a/custom_component/pollenprognos/translations/sv.json b/custom_component/pollenprognos/translations/sv.json new file mode 100644 index 0000000..c6bdd66 --- /dev/null +++ b/custom_component/pollenprognos/translations/sv.json @@ -0,0 +1,31 @@ +{ + "title": "Pollenprognos", + "config": { + "step": { + "user": { + "title": "Pollenprognos konfiguration", + "description": "HĂ€mtar aktuella pollenprognoser" + }, + "select_city": { + "title": "Pollenprognos - Stad", + "description": "VĂ€lj vilken stad du vill övervaka", + "data": { + "conf_city": "VĂ€lj stad" + } + }, + "select_pollen": { + "title": "Pollenprognos - Pollen", + "description": "VĂ€lja vilka pollen du vill övervaka", + "data": { + "conf_pollen": "VĂ€lj pollen" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "progress": { + "fetch_cities": "HĂ€mtar pollenprognoser" + } + } +} \ No newline at end of file diff --git a/custom_updater.json b/custom_updater.json deleted file mode 100644 index 494301a..0000000 --- a/custom_updater.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "sensor.pollenniva": { - "updated_at": "2019-05-07", - "version": "1.1.1", - "local_location": "/custom_components/pollenniva/__init__.py", - "remote_location": "https://raw.githubusercontent.com/JohNan/home-assistant-pollenkoll/master/pollenniva/__init__.py", - "visit_repo": "https://github.com/JohNan/home-assistant-pollenkoll", - "changelog": "https://github.com/JohNan/home-assistant-pollenkoll/releases/latest", - "resources": [ - "https://raw.githubusercontent.com/JohNan/home-assistant-pollenkoll/master/pollenniva/const.py", - "https://raw.githubusercontent.com/JohNan/home-assistant-pollenkoll/master/pollenniva/sensor.py", - "https://raw.githubusercontent.com/JohNan/home-assistant-pollenkoll/master/pollenniva/manifest.json" - ] - } -} diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..fb657d2 --- /dev/null +++ b/hacs.json @@ -0,0 +1,8 @@ +{ + "name": "Pollenprognos", + "iot_class": "Cloud Polling", + "homeassistant": "2021.3.0", + "hide_default_branch": true, + "zip_release": true, + "filename": "pollenprognos.zip" +} diff --git a/manage/update_manifest.py b/manage/update_manifest.py new file mode 100644 index 0000000..c540729 --- /dev/null +++ b/manage/update_manifest.py @@ -0,0 +1,25 @@ +"""Update the manifest file.""" +import sys +import json +import os + + +def update_manifest(): + """Update the manifest file.""" + version = "0.0.0" + for index, value in enumerate(sys.argv): + if value in ["--version", "-V"]: + version = sys.argv[index + 1] + + with open(f"{os.getcwd()}/custom_components/pollenprognos/manifest.json") as manifestfile: + manifest = json.load(manifestfile) + + manifest["version"] = version + + with open( + f"{os.getcwd()}/custom_components/pollenprognos/manifest.json", "w" + ) as manifestfile: + manifestfile.write(json.dumps(manifest, indent=4, sort_keys=True)) + + +update_manifest() \ No newline at end of file diff --git a/pollenniva/__init__.py b/pollenniva/__init__.py deleted file mode 100644 index 60d47d7..0000000 --- a/pollenniva/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""PollennivĂ„ Custom Component.""" diff --git a/pollenniva/const.py b/pollenniva/const.py deleted file mode 100644 index f4fe354..0000000 --- a/pollenniva/const.py +++ /dev/null @@ -1,2 +0,0 @@ -"""PollennivĂ„ Custom Update Version.""" -VERSION = '1.1.1' diff --git a/pollenniva/manifest.json b/pollenniva/manifest.json deleted file mode 100644 index 1d5c07d..0000000 --- a/pollenniva/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "pollenniva", - "name": "PollennivĂ„", - "documentation": "https://github.com/JohNan/home-assistant-pollenkoll", - "dependencies": [], - "codeowners": ["@JohNan"], - "requirements": [] - } \ No newline at end of file diff --git a/pollenniva/sensor.py b/pollenniva/sensor.py deleted file mode 100644 index f612e25..0000000 --- a/pollenniva/sensor.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -Support for getting current pollen levels from Pollenkollen.se -Visit https://pollenkoll.se/pollenprognos/ to find available cities -Visit https://pollenkoll.se/pollenprognos-ostersund/ to find available allergens - -Example configuration - -sensor: - - platform: pollenniva - scan_interval: 14400 # (default=14400 seconds (4 hours), optional) - state_as_string: false # (default=false, optional, show states as strings as per STATES below) - sensors: - - city: Stockholm - days_to_track: 4 # (default=4, values 1-4, optional) - allergens: - - GrĂ€s - - Hassel - - city: Östersund - allergens: - - Hassel -""" - -import logging -import json - -from collections import namedtuple -from datetime import timedelta - -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.components.rest.sensor import RestData -from homeassistant.const import (CONF_NAME, CONF_SCAN_INTERVAL) -from homeassistant.util import Throttle -from dateutil import parser -from datetime import datetime -from .const import VERSION - -_LOGGER = logging.getLogger(__name__) -_ENDPOINT = 'https://pollenkoll.se/wp-content/themes/pollenkoll/api/get_all.json' - -STATES = { - "i.h.": 0, - "L": 1, - "L-M": 2, - "M": 3, - "M-H": 4, - "H": 5, - "H-H+": 6, - "H+": 7 -} - -SENSOR_ICONS = { - 'Al': 'mdi:leaf', - 'Alm': 'mdi:leaf', - 'Asp': 'mdi:leaf', - 'Björk': 'mdi:leaf', - 'Ek': 'mdi:leaf', - 'GrĂ„bo': 'mdi:flower', - 'GrĂ€s': 'mdi:flower', - 'Hassel': 'mdi:leaf', - 'SĂ€lg': 'mdi:leaf', - 'default': 'mdi:leaf' -} - -DEFAULT_NAME = 'PollennivĂ„' -DEFAULT_STATE_AS_STRING = False -DEFAULT_DAYS_TO_TRACK = 3 - -CONF_SENSORS = 'sensors' -CONF_INTERVAL = 'scan_interval' -CONF_STATE_AS_STRING = 'state_as_string' -CONF_DAYS_TO_TRACK = 'days_to_track' -CONF_ALLERGENS = 'allergens' -CONF_CITY = 'city' - -SENSOR_SCHEMA = vol.Schema({ - vol.Required(CONF_CITY): cv.string, - vol.Optional(CONF_ALLERGENS, []): cv.ensure_list, - vol.Optional(CONF_DAYS_TO_TRACK, default=DEFAULT_DAYS_TO_TRACK): - vol.All(vol.Coerce(int), vol.Range(min=1, max=4), msg="Only a value between 0-4 is valid"), -}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_AS_STRING, default=DEFAULT_STATE_AS_STRING): cv.boolean, - vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), -}) - -SCAN_INTERVAL = timedelta(seconds=14400) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Pollen sensor.""" - name = config.get(CONF_NAME) - sensors = config.get(CONF_SENSORS) - state_as_string = config.get(CONF_STATE_AS_STRING) - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - - api = PollenkollAPI(scan_interval) - - devices = [] - - for sensor in sensors: - if CONF_DAYS_TO_TRACK in sensor: - days_to_track = sensor.get(CONF_DAYS_TO_TRACK, DEFAULT_DAYS_TO_TRACK) - for day in range(days_to_track): - for allergen in sensor[CONF_ALLERGENS]: - devices.append(PollennivaSensor(api, name, sensor, allergen, state_as_string, day)) - else: - for allergen in sensor[CONF_ALLERGENS]: - devices.append(PollennivaSensor(api, name, sensor, allergen, state_as_string)) - - add_devices(devices, True) - - -class PollennivaSensor(Entity): - """Representation of a Pollen sensor.""" - - def __init__(self, api, name, sensor, allergen, state_as_string, day=0): - """Initialize a Pollen sensor.""" - self._state_as_string = state_as_string - self._api = api - self._item = sensor - self._city = sensor['city'] - self._state = None - self._day = day - self._allergen = allergen - self._name = "{} {} {} day {}".format(name, self._city, self._allergen, str(self._day)) - self._attributes = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - if self._state is not None: - return self._state - return None - - @property - def device_state_attributes(self): - """Return the state attributes of the monitored installation.""" - if self._attributes is not None: - return self._attributes - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return "" - - @property - def icon(self): - """ Return the icon for the frontend.""" - if self._allergen in SENSOR_ICONS: - return SENSOR_ICONS[self._allergen] - return SENSOR_ICONS['default'] - - def update(self): - """Get the latest data from the API and updates the state.""" - pollen = {} - self._attributes = {} - self._api.update() - - for cities in self._api.data: - for city in cities['CitiesData']: - if city['name'] in self._city: - self._attributes.update({"last_modified": city['date_mod']}) - self._attributes.update({"city": city['name']}) - pollen = city['pollen'] - - for allergen in pollen: - if allergen['type'] == self._allergen: - day_value = 'day' + str(self._day) + '_value' - if day_value in allergen: - if self._state_as_string is False and allergen[day_value] in STATES: - value = STATES[allergen[day_value]] - else: - value = allergen[day_value] - self._state = value - self._attributes.update({"allergen": allergen['type']}) - self._attributes.update({"level": allergen[day_value]}) - self._attributes.update({"relative_day": allergen['day' + str(self._day) + '_relative_date']}) - self._attributes.update({"day": allergen['day' + str(self._day) + '_name']}) - self._attributes.update({"date": allergen['day' + str(self._day) + '_date']}) - self._attributes.update({"description": allergen['day' + str(self._day) + '_desc']}) - - -class PollenkollAPI: - """Get the latest data from Pollenkoll""" - - def __init__(self, interval): - self._rest = RestData('GET', _ENDPOINT, None, None, None, True) - self.data = None - self.available = True - self.update = Throttle(interval)(self._update) - - def _update(self): - """Get the data""" - try: - self._rest.update() - self.data = json.loads(self._rest.data) - except TypeError: - _LOGGER.error("Unable to fetch data from Pollenkoll. " + str(e)) - self.available = False