Skip to content
This repository has been archived by the owner on Nov 19, 2021. It is now read-only.

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
foosel committed Mar 14, 2020
0 parents commit 7165daf
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 0 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Coronavirus Hessen

[Home Assistant](https://www.home-assistant.io/) component to scrape the current SARS-CoV-2 data for the German state of Hessen from the [website of the Hessisches Ministerium für Soziales und Integration](https://soziales.hessen.de/gesundheit/infektionsschutz/coronavirus-sars-cov-2/taegliche-uebersicht-der-bestaetigten-sars-cov-2-faelle-hessen).

## Setup

<!--
There are two ways to set this up:
#### 1. Using HACS
Open your HACS Settings and add
https://github.com/foosel/homeassistant-coronavirus-hessen
as custom repository URL.
Then install the "Coronavirus Hessen" integration.
If you use this method, your component will always update to the latest version.
#### 2. Manual
-->

Copy the folder `custom_components/coronavirus_hessen` to `<ha_config_dir>/custom_components/`. When you are done you should have `<ha_config_dir>/custom_components/coronavirus_hessen/__init__.py`, `<ha_config_dir>/custom_components/coronavirus_hessen/sensor.py` and so on.

<!-- If you use this method then you'll need to keep an eye on this repository to check for updates. -->

## Configuration:

In Home Assistant:

1. Enter configuration menu
2. Select "Integrations"
3. Click the "+" in the bottom right
4. Choose "Coronavirus Hessen"
5. Choose the county you wish to monitor (or "Gesamthessen" for all of Hessen)
6. Save

## TODO

* [ ] Find out why the created sensors don't show up in the integration overview
* [ ] Find out if there's a possibility to select more than one county during configuration to have all created sensors under *one* integration entry
* [ ] Make this thing work with HACS for easier installation/updating

*This is my first integration for Home Assistant ever and I basically learned how to even begin to do this stuff while writing this. I'm happy for any pointers as to how to improve things.*
16 changes: 16 additions & 0 deletions custom_components/coronavirus_hessen/.translations/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"config": {
"title": "Coronavirus Hessen",
"step": {
"user": {
"title": "Wähle einen Landkreis",
"data": {
"country": "Landkreis"
}
}
},
"abort": {
"already_configured": "Dieser Landkreis ist bereits konfiguriert."
}
}
}
16 changes: 16 additions & 0 deletions custom_components/coronavirus_hessen/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"config": {
"title": "Coronavirus Hessen",
"step": {
"user": {
"title": "Pick a county to monitor",
"data": {
"country": "County"
}
}
},
"abort": {
"already_configured": "This county is already configured."
}
}
}
105 changes: 105 additions & 0 deletions custom_components/coronavirus_hessen/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""The corona_hessen component."""

from datetime import timedelta
import logging

import async_timeout
import asyncio
import bs4

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback

from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, ENDPOINT, OPTION_TOTAL

_LOGGER = logging.getLogger(__name__)

PLATFORMS = ["sensor"]

async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Coronavirus Hessen component."""
# Make sure coordinator is initialized.
await get_coordinator(hass)
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Coronavirus Hessen from a config entry."""

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

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)

return unload_ok


async def get_coordinator(hass):
"""Get the data update coordinator."""
if DOMAIN in hass.data:
return hass.data[DOMAIN]

async def async_get_data():
with async_timeout.timeout(10):
response = await aiohttp_client.async_get_clientsession(hass).get(ENDPOINT)
raw_html = await response.text()

data = bs4.BeautifulSoup(raw_html, "html.parser")

result = dict()
rows = data.select("article table:first-of-type tr")

# Counties
for row in rows[1:-1]:
line = row.select("td")
if len(line) != 4:
continue

try:
county = line[0].text.strip()
cases_str = line[3].text.strip()
if len(cases_str) and cases_str != "-":
cases = int(cases_str)
else:
cases = 0
except ValueError:
_LOGGER.error("Error processing line {}, skipping".format(line))
continue
result[county] = cases

# Total
line = rows[-1].select("td")
try:
result[OPTION_TOTAL] = int(line[-1].select("p strong")[0].text.strip())
except ValueError:
_LOGGER.error("Error processing total value from {}, skipping".format(line))

_LOGGER.debug("Corona Hessen: {!r}".format(result))
return result

hass.data[DOMAIN] = DataUpdateCoordinator(
hass,
logging.getLogger(__name__),
name=DOMAIN,
update_method=async_get_data,
update_interval=timedelta(hours=12), # 12h as the data apparently only updates once per day anyhow
)
await hass.data[DOMAIN].async_refresh()
return hass.data[DOMAIN]
45 changes: 45 additions & 0 deletions custom_components/coronavirus_hessen/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Config flow for Coronavirus Hessen integration."""
import logging

import voluptuous as vol

from homeassistant import config_entries

from . import get_coordinator
from .const import DOMAIN, OPTION_TOTAL

_LOGGER = logging.getLogger(__name__)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Coronavirus Hessen."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

_options = None

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if self._options is None:
self._options = {OPTION_TOTAL: "Gesamthessen"}
coordinator = await get_coordinator(self.hass)
for county in sorted(coordinator.data.keys()):
if county == OPTION_TOTAL:
continue
self._options[county] = county

if user_input is not None:
await self.async_set_unique_id(user_input["county"])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=self._options[user_input["county"]], data=user_input
)

_LOGGER.debug("Showing config form, options is {!r}".format(self._options))
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required("county"): vol.In(self._options)
}),
)
5 changes: 5 additions & 0 deletions custom_components/coronavirus_hessen/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Constants for the Coronavirus Hessen integration."""
DOMAIN = "coronavirus_hessen"
ENDPOINT = "https://soziales.hessen.de/gesundheit/infektionsschutz/coronavirus-sars-cov-2/taegliche-uebersicht-der-bestaetigten-sars-cov-2-faelle-hessen"
ATTRIBUTION = "Data provided by Hessisches Ministrium für Soziales und Integration"
OPTION_TOTAL = "total"
9 changes: 9 additions & 0 deletions custom_components/coronavirus_hessen/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"domain": "coronavirus_hessen",
"name": "Coronavirus Hessen",
"config_flow": true,
"documentation": "https://github.com/foosel/homeassistant-coronavirus-hessen",
"requirements": ["beautifulsoup4==4.8.2"],
"dependencies": [],
"codeowners": ["@foosel"]
}
63 changes: 63 additions & 0 deletions custom_components/coronavirus_hessen/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Support for getting current Corona data from the website of the Hessische Ministerium für Soziales und Integration."""
import logging

import voluptuous as vol

from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.entity import Entity

from . import get_coordinator
from .const import ATTRIBUTION, OPTION_TOTAL

_LOGGER = logging.getLogger(__name__)

async def async_setup_entry(hass, config_entry, async_add_entities):
"""Defer sensor setup to the shared sensor module."""
coordinator = await get_coordinator(hass)

async_add_entities([CoronaHessenSensor(coordinator, config_entry.data["county"])])

class CoronaHessenSensor(Entity):
"""Representation of a county with Corona cases."""

def __init__(self, coordinator, county):
"""Initialize sensor."""
self.coordinator = coordinator
self.county = county
if county == OPTION_TOTAL:
self._name = f"Coronavirus Hessen"
else:
self._name = f"Coronavirus Hessen {county}"
self._state = None

@property
def available(self):
return self.coordinator.last_update_success and self.county in self.coordinator.data

@property
def name(self):
return self._name

@property
def icon(self):
return "mdi:biohazard"

@property
def unit_of_measurement(self):
return "people"

@property
def state(self):
return self.coordinator.data[self.county]

@property
def device_state_attributes(self):
return {ATTR_ATTRIBUTION: ATTRIBUTION}

async def async_added_to_hass(self):
"""When entity is added to hass."""
self.coordinator.async_add_listener(self.async_write_ha_state)

async def async_will_remove_from_hass(self):
"""When entity will be removed from hass."""
self.coordinator.async_remove_listener(self.async_write_ha_state)
16 changes: 16 additions & 0 deletions custom_components/coronavirus_hessen/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"config": {
"title": "Coronavirus Hessen",
"step": {
"user": {
"title": "Pick a county to monitor",
"data": {
"country": "County"
}
}
},
"abort": {
"already_configured": "This county is already configured."
}
}
}

0 comments on commit 7165daf

Please sign in to comment.