diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..3205926 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,6 +12,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +25,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3eddd09 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b7b544c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +name: Release Workflow + +on: + release: + types: [published] + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: πŸ“₯ Checkout the repository + uses: actions/checkout@v3 + + - name: πŸ”’ Get release version + id: version + uses: home-assistant/actions/helpers/version@master + + - name: ℹ️ Get integration information + id: information + run: | + name=$(find custom_components/ -type d -maxdepth 1 | tail -n 1 | cut -d "/" -f2) + echo "::set-output name=name::$name" + - name: πŸ–ŠοΈ Set version number + run: | + jq '.version = "${{ steps.version.outputs.version }}"' \ + "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json" > tmp \ + && mv -f tmp "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json" + - name: πŸ‘€ Validate data + run: | + manifestversion=$(jq -r '.version' ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json) + if [ "$manifestversion" != "${{ steps.version.outputs.version }}" ]; then + echo "The version in custom_components/${{ steps.information.outputs.name }}/manifest.json was not correct" + echo "$manifestversion" + exit 1 + fi + + - name: πŸ”’ Autobump version + run: | + VERSION=${{ steps.version.outputs.version }} + PLACEHOLDER='__version__ = "develop"' + VERSION_FILE=custom_components/${{ steps.information.outputs.name }}/version.py + # ensure the placeholder is there. If grep doesn't find the placeholder + # it exits with exit code 1 and github actions aborts the build. + grep "$PLACEHOLDER" "$VERSION_FILE" + sed -i "s/$PLACEHOLDER/__version__ = \"${VERSION}\"/g" "$VERSION_FILE" + shell: bash + + - name: πŸ“¦ Create zip file for the integration + run: | + cd "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}" + zip ${{ steps.information.outputs.name }}.zip -r ./ + - name: πŸ“€ Upload the zip file as a release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/${{ steps.information.outputs.name }}.zip" + asset_name: ${{ steps.information.outputs.name }}.zip + asset_content_type: application/zip diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index dc3e69c..22e29bf 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -12,25 +12,24 @@ on: jobs: validate-hassfest: - runs-on: ubuntu-latest + runs-on: "ubuntu-latest" name: With hassfest steps: - name: πŸ“₯ Checkout the repository - uses: actions/checkout@v2 + uses: "actions/checkout@v3" - name: βœ… Hassfest validation uses: "home-assistant/actions/hassfest@master" validate-hacs: - runs-on: ubuntu-latest + runs-on: "ubuntu-latest" name: With HACS Action steps: - name: πŸ“₯ Checkout the repository - uses: actions/checkout@v2 + uses: "actions/checkout@v3" - name: βœ… HACS validation - uses: hacs/action@main + uses: "hacs/action@main" with: - category: integration - comment: false - #ignore: brands wheels + category: "integration" + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..07a0e80 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changes + +## 2022.06.0 (02/06/2022) + +* Migrated to a new async library. Thanks @exxamalte, for your hard work. +* Migrated to `DataUpdateCoordinator` to reduce code complexity and improve performance. +* Migrated geo_location platform to integration with config flow. +* Added INGV Earthquakes sensor. +* Added depth, mode and status to geo_location entities attributes. +* Deleted title and external_id from geo_location entities attributes. + +## 2022.02.0 (27/02/2022) + +* Bump georss-ingv-centro-nazionale-terremoti-client v0.6. + +## 2021.06.0 (08/06/2021) + +* Bump georss-ingv-centro-nazionale-terremoti-client v0.5. + +## 2021.04.1 (29/04/2021) + +* Added iot_class to manifest. + +## 2021.04.0 (20/04/2021) + +* Fixed image URLs by supporting new pattern. + +## 2021.03.1 (28/03/2021) + +* Update typing and changed the tag version. + +## 1.0.3 (18/02/2021) + +* Added version to manifest.json and some enhanced code. + +## 1.0.2 (23/11/2020) + +* Added more informations about integration #6. + +## 1.0.1 (08/11/2020) + +* Fixed path of preview images (HACS information). + +## 1.0.0 (27/10/2020) + +* First release. All credit goes to Malte Franken [@exxamalte]. diff --git a/README.md b/README.md index e31be11..6fb9dac 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# INGV Terremoti +# INGV Earthquakes

@@ -8,31 +8,41 @@ [![GitHub latest release]][githubrelease] ![GitHub Release Date] [![Maintenancebadge]][Maintenance] [![GitHub issuesbadge]][GitHub issues] -[![Websitebadge]][website] [![Forum][forumbadge]][forum] [![telegrambadge]][telegram] [![facebookbadge]][facebook] +[![Websitebadge]][website] [![Forum][forumbadge]][forum] [![telegrambadge]][telegram] [![facebookbadge]][facebook] [![Don't buy me a coffee](https://img.shields.io/static/v1.svg?label=Don't%20buy%20me%20a%20coffee&message=πŸ””&color=black&logo=buy%20me%20a%20coffee&logoColor=white&labelColor=6f4e37)](https://paypal.me/hassiohelp) -Instructions on how to integrate the Istituto Nazionale di Geofisica e Vulcanologia (Earthquakes) Feed feed into Home Assistant. +Instructions on how to integrate the INGV Earthquakes feed into Home Assistant. All credit goes to Malte Franken [@exxamalte](https://github.com/exxamalte). -The `ingv_centro_nazionale_terremoti` platform lets you integrate a GeoRSS feed provided by the -Italian [Istituto Nazionale di Geofisica e Vulcanologia](http://www.ingv.it/it/) with information -about seismic events like earthquakes on the Italian Peninsula. -It retrieves incidents from a feed and shows information of those +The `ingv_centro_nazionale_terremoti` integration lets you use a QuakeML feeds provided by the +Italian [Istituto Nazionale di Geofisica e Vulcanologia](http://www.ingv.it/it/) with information +about seismic events like earthquakes on the Italian Peninsula. +It retrieves incidents from a feed and shows information of those incidents filtered by distance to Home Assistant's location. -Entities are generated, updated and removed automatically with each update -from the feed. Each entity defines latitude and longitude and will be shown -on the default map automatically, or on a map card by defining the source -`ingv_centro_nazionale_terremoti`. The distance in kilometers is available as the state +Entities are generated, updated and removed automatically with each update +from the feed. Each entity defines latitude and longitude and will be shown +on the default map automatically, or on a map card by defining the source +`ingv_centro_nazionale_terremoti`. The distance in kilometers is available as the state of each entity.

-The data is updated every 5 minutes. +The data is updated every 5 minutes and retrieve all events from the last 24 hours by default. + +
+ +The material used by this integration is provided under the [Creative Commons Attribution 4.0 International](http://creativecommons.org/licenses/by/4.0/). +It has only been modified for the purpose of presenting the material in Home Assistant. +Please refer to the [creator's disclaimer notice](hhttp://terremoti.ingv.it/en/webservices_and_software) and [Terms of service](http://www.fdsn.org/webservices/) for more information. + +We acknowledge the INGV and ISIDe Working Group at National Earthquake Observatory project and its sponsors by the Italian Presidenza del Consiglio dei Ministri, Dipartimento della Protezione Civile, for providing data/images used in this integration. + +
## How to install @@ -41,60 +51,100 @@ The data is updated every 5 minutes. you can copy the entire **ingv_centro_nazionale_terremoti** folder into **custom_components** folder in your root directory. You will need to create the dir **custom_components** if this is your first custom component. 2. Restart Home Assistant. -3. Then, add the following lines to your `configuration.yaml`: + +## Configuration + +### Config flow user interface + +To configure this integration go to: `Configurations` -> `Integrations` -> `ADD INTEGRATIONS` button, search for `INGV` and configure the component. + +You can also use following [My Home Assistant](http://my.home-assistant.io/) link + +[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=ingv_centro_nazionale_terremoti) + +### Config yaml + +1. Add the following lines to your `configuration.yaml`: ```yaml # Example configuration.yaml entry - geo_location: - - platform: ingv_centro_nazionale_terremoti + ingv_centro_nazionale_terremoti: + location: "Home" ``` -4. Save it. -5. Restart again Home Assistant. +2. Save it. +3. Restart again Home Assistant. > NOTE: > In an environment other than HassOS, you will probably need to install the dependencies manually. > Activate Python environment Home Assistant is running in and use following command: > -> `python3 -m pip install georss-ingv-centro-nazionale-terremoti-client` +> `python3 -m pip install aio_quakeml_ingv_centro_nazionale_terremoti_client` ### CONFIGURATION VARIABLES | Variables | Type | Requirement | Default | Description | |--------------------|-------------|---------------|------------|--------------| -|**minimum_magnitude**| float | optional | 0.0 | The minimum magnitude of an earthquake to be included. -|**radius**| float | optional | 50.0 | The distance in kilometers around Home Assistant's coordinates in which seismic events are included. +|**location**| string | optional | Location name defined in your `configuration.yaml` | Location name. |**latitude**| string | optional | Latitude defined in your `configuration.yaml` | Latitude of the coordinates around which events are considered. |**longitude**| string | optional | Longitude defined in your `configuration. yaml` | Longitude of the coordinates around which events are considered. +|**radius**| float | optional | 50.0 | The distance in kilometers around Home Assistant's coordinates in which seismic events are included. +|**minimum_magnitude**| float | optional | 3.0 | The minimum magnitude of an earthquake to be included. +|**scan_interval**| int | optional | 300 | The time in seconds for each update. +|**start_time**| int | optional | 24 | The start-time delta in hours. (ex last 24 hours) ## State Attributes -The following state attributes are available for each entity in addition to -the standard ones: +The following state attributes are available for each entity in addition to the standard ones: | Attribute | Description | |--------------------|-------------| | latitude | Latitude of the earthquake. | | longitude | Longitude of the earthquake. | | source | `ingv_centro_nazionale_terremoti` to be used in conjunction with `geo_location` automation trigger. | -| external_id | The external ID used in the feed to identify the earthquake in the feed. | -| title | Original title from the feed. | | region | Textual description of named geographic region near to the event. | | magnitude | Reported magnitude of the earthquake. | +| depth | The depth of the quake in km. | +| status | The Evaluation Status of the quake (preliminary, confirmed, reviewed, final, rejected). | +| mode | The Evaluation Mode of the quake (manual or automatic). | | publication_date | Date and time when this event occurred. | -| event_id | Return the short id of the event. | -| image_url | URL to a map supplied in the feed marking the location of the event. This could for example be used in notifications. **Images are only available for magnitude >= 3**. | +| event_id | Return the short id used in the feed to identify the earthquake in the feed. | +| image_url | URL for a map not provided in the feed that marks the location of the event. This could for example be used in notifications. **Images are only available for magnitude >= 3**. | + +![geo_location](https://github.com/caiosweet/Home-Assistant-custom-components-INGV/blob/main/assets/images/geo_location.png) + +## Sensor + +This integration automatically creates a sensor that shows how many entities +are currently managed by this integration. In addition to that the sensor has +some useful attributes that indicate the currentness of the data retrieved +from the feed. + +![sensor](https://github.com/caiosweet/Home-Assistant-custom-components-INGV/blob/main/assets/images/sensor.png) + +| Attribute | Description | +|------------------------|-------------| +| status | Status of last update from the feed ("OK" or "ERROR"). | +| last update | Timestamp of the last update from the feed. | +| last update successful | Timestamp of the last successful update from the feed. | +| last timestamp | Timestamp of the latest entry from the feed. | +| created | Number of entities that were created during last update (optional). | +| updated | Number of entities that were updated during last update (optional). | +| removed | Number of entities that were removed during last update (optional). | ## Full Configuration ```yaml # Example configuration.yaml entry -geo_location: - - platform: ingv_centro_nazionale_terremoti - radius: 100 - minimum_magnitude: 2.0 - latitude: 41.89 - longitude: 12.51 +ingv_centro_nazionale_terremoti: + location: "Home" + latitude: 41.89 + longitude: 12.51 + radius: 100 + minimum_magnitude: 2.0 + scan_interval: 300 + start_time: 24 + ``` ___ @@ -103,63 +153,6 @@ ___ ## [My Package](https://github.com/caiosweet/Package-Natural-Events/tree/main/config/packages) -## Example Binary Sensor - -```yaml -binary_sensor: - - platform: template - sensors: - lastquake: - friendly_name: Evento terremoto - device_class: vibration - # availability_template: False - value_template: >- - {% set last_date = states.geo_location - | selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - | sort(attribute='attributes.publication_date') - | map(attribute='attributes.publication_date') |list|last|default %} - {{ ((as_timestamp(utcnow())-as_timestamp(last_date))/3600) <= 24 if last_date else False }} - attribute_templates: - distance: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='state')|list|last|default}} - lat: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.latitude')|list|last|default}} - long: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.longitude')|list|last|default}} - title: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.title')|list|last|default}} - region: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.region')|list|last|default}} - magnitude: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.magnitude')|list|last|default}} - publication_date: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.publication_date')|list|last|default}} - event_id: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.event_id')|list|last|default}} - image_url: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.image_url')|list|last|default}} - attribution: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.attribution')|list|last|default}} - level: >- - {%set m = states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.magnitude')|list|last|default(0)%} - {% set m = m|float %} - {%if 0<=m<=1.9%}0{%elif 2<=m<=2.9%}1{%elif 3<=m<=3.9%}2{%elif 4<=m<=5.9%}3{%else%}4{%endif%} - external_id: >- - {{states.geo_location|selectattr('attributes.source','eq','ingv_centro_nazionale_terremoti') - |sort(attribute='attributes.publication_date')|map(attribute='attributes.external_id')|list|last|default|replace('smi:','')}} -``` - ## Example Zone ```yaml @@ -175,40 +168,26 @@ zone: ```yaml automation: - - alias: Quake Notifications - mode: queued - max_exceeded: silent - initial_state: true - trigger: - - platform: geo_location - source: "ingv_centro_nazionale_terremoti" - zone: zone.geoalert - event: enter - condition: >- - {{ ((as_timestamp(utcnow()) - as_timestamp(trigger.to_state.attributes.publication_date))/3600*60)|int < 90 }} - action: - - service: notify.telegram - data: - title: >- - 🚧 Rilevato terremoto. - message: >- - {% set data_utc = trigger.to_state.attributes.publication_date %} - Rilevato terremoto di magnitudo: {{ trigger.to_state.attributes.magnitude }} - a una distanza di {{ trigger.to_state.state }} Km da casa. Epicentro: {{ trigger.to_state.attributes.region }} - {{ as_timestamp(data_utc)|timestamp_custom ('Data %d/%m/%Y Ore %H:%M:%S') }} - {% if trigger.to_state.attributes.image_url is defined %} - {{ trigger.to_state.attributes.image_url }} - {% endif %} - - choose: - - conditions: "{{ trigger.to_state.attributes.image_url is defined }}" - sequence: - - service: telegram_bot.send_photo - data: - url: "{{ trigger.to_state.attributes.image_url }}" - caption: "{{ trigger.to_state.attributes.title }}" - target: '12345' - parse_mode: html - timeout: 1000 + alias: INGV Quakes Notification Send + description: '' + trigger: + - platform: geo_location + source: ingv_centro_nazionale_terremoti + zone: zone.geoalert + event: enter + condition: [] + action: + - service: notify.discord + data: + title: New INGV Quakes + message: | + Rilevato terremoto a una distanza di {{trigger.to_state.state}} Km da + casa. Magnitudo: {{trigger.to_state.attributes.magnitude}} Epicentro: + {{trigger.to_state.attributes.region}} Profondità: + {{trigger.to_state.attributes.depth}} km. {% set data_utc = + trigger.to_state.attributes.publication_date %} + {{as_timestamp(data_utc)|timestamp_custom('%H:%M:%S - %d/%m/%Y')}} + mode: queued ``` ## Example Lovelace Map Card @@ -225,111 +204,6 @@ aspect_ratio: '16:9' hours_to_show: 72 ``` -## Example My Lovelace card - -Required custom auto-entities, card-mod and [binary_sensor.lastquake](#example-binary-sensor) - -```yaml -type: conditional # TERREMOTO conditional -conditions: - - entity: binary_sensor.lastquake - state: "on" -card: - type: vertical-stack - cards: - - type: markdown - entity_id: - - binary_sensor.lastquake - card_mod: - style: | - ha-card {background: none; border-radius: 0px; box-shadow: none;} - ha-markdown {padding-bottom: 0 !important;} - content: >- - ___ - - #### TERREMOTI - [ Hai Sentito Il Terremoto](http://www.haisentitoilterremoto.it/) - - {%- set url = "http://shakemap.rm.ingv.it/shake4/data/{}/current/products/{}.jpg" -%} - {%- set url2 = "http://shakemap.ingv.it/shake4/data/{}/current/products/{}.jpg" -%} - {%- set entityid = 'binary_sensor.lastquake' -%} - {%- set id = state_attr(entityid, 'event_id') -%} - {%- set magnitudo = state_attr(entityid, 'magnitude')|float(default=0) -%} - {%- set code = {0:'White', 1:'Green', 2:'Gold', 3:'Orange', 4:'Red'} -%} - {%- set color = code[state_attr('binary_sensor.lastquake', 'level')|int(0)] -%} - {%- set lat = state_attr(entityid, 'lat') -%} - {%- set long = state_attr(entityid, 'long') -%} - {%- set utc = as_timestamp(state_attr(entityid, 'publication_date'),0) -%} - - - **{{ utc|timestamp_custom('%H:%M:%S del %d/%m/%Y') if utc is not none else 0}}**

- Un terremoto di magnitudo **{{magnitudo}}**
- Γ¨ avvenuto nella zona: [{{state_attr(entityid, 'region')}}](https://www.openstreetmap.org/?mlat={{lat}}&mlon={{long}}#map=12/{{lat}}/{{long}})
- a **{{state_attr(entityid, 'distance')}}** km da casa,
- con coordinate epicentrali {{lat}}, {{long}}. - - {% set state_dict = {'home': 'casa', 'not_home': 'fuori casa', 'unknown': '❓'} %} - {% for person in expand(states.person) %} - {% if 'latitude' in person.attributes and person.attributes.latitude is not none %} - {% set distanza = distance(lat|default(0), long|default(0), person.entity_id|default(0)) %} -
{{"πŸ“{} ({}) a circa {} km dall'epicentro.".format(person.name|upper, state_dict.get(person.state, person.state), distanza|round(1, default=0)) }} - {% else %} -
{{"πŸ“{} ({})".format(person.name|upper, state_dict.get(person.state, person.state)) }} - {% endif %} - {% endfor %}

- - {% if magnitudo >= 3 %} - [Intensity]({{url.format(id,'intensity')}}) ~ - [PGA]({{url.format(id,'pga')}}) ~ [PGV]({{url.format(id,'pgv')}}) ~ [PSA0]({{url.format(id,'psa0p3')}}) ~ [PSA1]({{url.format(id,'psa1p0')}}) ~ - [HSIT](http://eventi.haisentitoilterremoto.it/{{id}}/{{id}}_mcs.jpg)
- - - - {% if is_state('binary_sensor.download_file', 'on') %} - - {% else %} - - {% endif %}{% endif %} - -
- - - - - - type: custom:auto-entities # CONDITIONAL ULTIMI {count} TERREMOTI - show_empty: false - sort: - { - attribute: publication_date, - method: attribute, - reverse: true, - count: 4, - first: 0, - } - filter: - include: - - entity_id: geo_location.* - attributes: - source: ingv_centro_nazionale_terremoti - card: - type: entities - entity_id: this.entity_id - card_mod: - style: | - ha-card {background: none; border-radius: 0px; box-shadow: none;} - -``` -

@@ -338,7 +212,6 @@ card: All product names, trademarks and registered trademarks in the images in this repository, are property of their respective owners. All images in this repository are used by the author for identification purposes only. The use of these names, trademarks and brands appearing in these image files, do not imply endorsement. - [hacs]: https://github.com/custom-components/hacs [hacsbadge]: https://img.shields.io/badge/HACS-Default-orange.svg diff --git a/assets/images/geo_location.png b/assets/images/geo_location.png new file mode 100644 index 0000000..d2e9b8f Binary files /dev/null and b/assets/images/geo_location.png differ diff --git a/assets/images/sensor.png b/assets/images/sensor.png new file mode 100644 index 0000000..a9003c4 Binary files /dev/null and b/assets/images/sensor.png differ diff --git a/custom_components/ingv_centro_nazionale_terremoti/__init__.py b/custom_components/ingv_centro_nazionale_terremoti/__init__.py index 65d4549..d18f9d8 100644 --- a/custom_components/ingv_centro_nazionale_terremoti/__init__.py +++ b/custom_components/ingv_centro_nazionale_terremoti/__init__.py @@ -1,2 +1,211 @@ -"""The ingv_centro_nazionale_terremoti component.""" -"""All credit goes to Malte Franken [@exxamalte].""" +"""The INGV Earthquakes integration.""" +"""All credit goes to Malte Franken [@exxamalte].""" +from collections.abc import Callable +from datetime import timedelta +import logging + +from aio_quakeml_ingv_centro_nazionale_terremoti_client import ( + IngvCentroNazionaleTerremotiQuakeMLFeedManager, +) + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_MILES, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.unit_system import METRIC_SYSTEM + +from .const import ( + CONF_MINIMUM_MAGNITUDE, + CONF_START_TIME, + DEFAULT_MINIMUM_MAGNITUDE, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DEFAULT_START_TIME, + DOMAIN, + FEED, + PLATFORMS, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the INGV Earthquakes integration.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + location = conf.get(CONF_LOCATION, hass.config.location_name) + latitude = conf.get(CONF_LATITUDE, hass.config.latitude) + longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + radius = conf.get(CONF_RADIUS, DEFAULT_RADIUS) + magnitude = conf.get(CONF_MINIMUM_MAGNITUDE, DEFAULT_MINIMUM_MAGNITUDE) + start_time = conf.get(CONF_START_TIME, DEFAULT_START_TIME) + scan_interval = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_LOCATION: location, + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_RADIUS: radius, + CONF_MINIMUM_MAGNITUDE: magnitude, + CONF_SCAN_INTERVAL: scan_interval, + CONF_START_TIME: start_time, + }, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the INGV Earthquakes integration from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + feeds = hass.data[DOMAIN].setdefault(FEED, {}) + radius = entry.options[CONF_RADIUS] + if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + # Create feed entity coordinator for all platforms. + coordinator = IngvDataUpdateCoordinator(hass=hass, entry=entry, radius_in_km=radius) + feeds[entry.entry_id] = coordinator + _LOGGER.debug("Feed entity coordinator added for %s", entry.entry_id) + + async_cleanup_entity_registry(hass=hass, entry=entry) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + return True + + +@callback +def async_cleanup_entity_registry( + hass: HomeAssistant, + entry: ConfigEntry, +) -> None: + """Remove orphaned geo_location entities.""" + entity_registry = er.async_get(hass) + orphaned_entries = er.async_entries_for_config_entry( + registry=entity_registry, config_entry_id=entry.entry_id + ) + if orphaned_entries is not None: + for orphan in orphaned_entries: + if orphan.domain == Platform.GEO_LOCATION: + _LOGGER.debug("Removing orphaned entry %s", orphan.entity_id) + entity_registry.async_remove(orphan.entity_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + coordinator = hass.data[DOMAIN][FEED].pop(entry.entry_id) + await coordinator.async_stop() + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle an options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +class IngvDataUpdateCoordinator(DataUpdateCoordinator): + """Data update coordinator for the INGV Earthquakes integration.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, radius_in_km: float) -> None: + """Initialize the Feed Entity Coordinator.""" + self.entry = entry + self.hass = hass + coordinates = ( + entry.data[CONF_LATITUDE], + entry.data[CONF_LONGITUDE], + ) + websession = async_get_clientsession(hass) + self._feed_manager = IngvCentroNazionaleTerremotiQuakeMLFeedManager( + websession=websession, + coordinates=coordinates, + filter_radius=radius_in_km, + filter_minimum_magnitude=entry.options[CONF_MINIMUM_MAGNITUDE], + starttime_delta=timedelta(hours=entry.options[CONF_START_TIME]), + generate_async_callback=self._generate_entity, + update_async_callback=self._update_entity, + remove_async_callback=self._remove_entity, + status_async_callback=self._status_update, + ) + self._entry_id = entry.entry_id + self._status_info = None + self.listeners: list[Callable[[], None]] = [] + super().__init__( + hass=hass, + logger=_LOGGER, + name=f"{DOMAIN}-{entry.data[CONF_LOCATION]}", + update_method=self.async_update, + update_interval=timedelta(seconds=entry.options[CONF_SCAN_INTERVAL]), + ) + + async def async_update(self) -> None: + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity coordinator updated") + return self._feed_manager.feed_entries + + async def async_stop(self) -> None: + """Stop this feed entity coordinator from refreshing.""" + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + _LOGGER.debug("Feed entity coordinator stopped") + + @callback + def async_event_new_entity(self) -> str: + """Return coordinator specific event to signal new entity.""" + return f"{DOMAIN}_new_geolocation_{self._entry_id}" + + def get_entry(self, external_id): + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) + + def entry_available(self, external_id) -> bool: + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) is not None + + def status_info(self): + """Return latest status update info received.""" + return self._status_info + + async def _generate_entity(self, external_id: str) -> None: + """Generate new entity.""" + _LOGGER.debug("New entry received for: %s", external_id) + async_dispatcher_send( + self.hass, + self.async_event_new_entity(), + self, + self.entry.unique_id, + external_id, + ) + + async def _update_entity(self, external_id: str) -> None: + """Ignore update call; this is handled by the coordinator.""" + + async def _remove_entity(self, external_id: str) -> None: + """Remove entity.""" + _LOGGER.debug("Remove received for: %s", external_id) + async_dispatcher_send(self.hass, f"{DOMAIN}_delete_{external_id}") + + async def _status_update(self, status_info) -> None: + """Store status update.""" + _LOGGER.debug("Status update received: %s", status_info) + self._status_info = status_info diff --git a/custom_components/ingv_centro_nazionale_terremoti/config_flow.py b/custom_components/ingv_centro_nazionale_terremoti/config_flow.py new file mode 100644 index 0000000..6c14352 --- /dev/null +++ b/custom_components/ingv_centro_nazionale_terremoti/config_flow.py @@ -0,0 +1,139 @@ +"""Config flow for INGV Earthquakes integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, +) +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_MINIMUM_MAGNITUDE, + CONF_START_TIME, + DEFAULT_MINIMUM_MAGNITUDE, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DEFAULT_START_TIME, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class IngvConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for INGV Earthquakes integration.""" + + VERSION = 1 + + async def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult: + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_LOCATION, default=self.hass.config.location_name): str, + vol.Optional(CONF_LATITUDE, default=self.hass.config.latitude): cv.latitude, + vol.Optional(CONF_LONGITUDE, default=self.hass.config.longitude): cv.longitude, + } + ), + errors=errors or {}, + ) + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + _LOGGER.debug("Import config from: %s", import_config.get("platform", DOMAIN)) + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle the start of the config flow.""" + _LOGGER.debug("User input: %s", user_input) + + if not user_input: + return await self._show_form() + + location = user_input.get(CONF_LOCATION, self.hass.config.location_name) + await self.async_set_unique_id(location) + self._abort_if_unique_id_configured() + + latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) + longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) + + data = { + CONF_LOCATION: location, + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + } + + magnitude = user_input.get(CONF_MINIMUM_MAGNITUDE, DEFAULT_MINIMUM_MAGNITUDE) + radius = user_input.get(CONF_RADIUS, DEFAULT_RADIUS) + scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + start_time = user_input.get(CONF_START_TIME, DEFAULT_START_TIME) + + options = { + CONF_MINIMUM_MAGNITUDE: magnitude, + CONF_RADIUS: radius, + CONF_SCAN_INTERVAL: scan_interval, + CONF_START_TIME: start_time, + } + + return self.async_create_entry(title=location, data=data, options=options) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> IngvOptionsFlow: + """Get the options flow for this handler.""" + return IngvOptionsFlow(config_entry) + + +class IngvOptionsFlow(OptionsFlow): + """Handle a option flow for INGV Earthquakes integration.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.options = config_entry.options + + async def async_step_init( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Handle options flow.""" + if not user_input: + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_MINIMUM_MAGNITUDE, + default=self.options.get( + CONF_MINIMUM_MAGNITUDE, DEFAULT_MINIMUM_MAGNITUDE + ), + ): cv.positive_float, + vol.Optional( + CONF_RADIUS, default=self.options.get(CONF_RADIUS, DEFAULT_RADIUS) + ): vol.Coerce(float), + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL), + ): cv.positive_int, + vol.Optional( + CONF_START_TIME, + default=self.options.get(CONF_START_TIME, DEFAULT_START_TIME), + ): cv.positive_int, + } + ), + ) + + return self.async_create_entry(title="", data=user_input) diff --git a/custom_components/ingv_centro_nazionale_terremoti/const.py b/custom_components/ingv_centro_nazionale_terremoti/const.py new file mode 100644 index 0000000..2762151 --- /dev/null +++ b/custom_components/ingv_centro_nazionale_terremoti/const.py @@ -0,0 +1,37 @@ +"""Define constants for the INGV Earthquakes integration.""" +from typing import Final + +from homeassistant.const import Platform + +from .version import __version__ + +DOMAIN: Final = "ingv_centro_nazionale_terremoti" +DEFAULT_NAME: Final = "INGV Earthquakes" + +ATTR_CREATED: Final = "created" +ATTR_LAST_UPDATE: Final = "last_update" +ATTR_LAST_UPDATE_SUCCESSFUL: Final = "last_update_successful" +ATTR_LAST_TIMESTAMP: Final = "last_timestamp" +ATTR_REMOVED: Final = "removed" +ATTR_STATUS: Final = "status" +ATTR_UPDATED: Final = "updated" + +CONF_MINIMUM_MAGNITUDE: Final = "minimum_magnitude" +CONF_START_TIME: Final = "start_time" + +DEFAULT_FORCE_UPDATE: Final = True +DEFAULT_MINIMUM_MAGNITUDE: Final = 3.0 +DEFAULT_RADIUS: Final = 50.0 +DEFAULT_SCAN_INTERVAL: Final = 300 +DEFAULT_START_TIME: Final = 24 +DEFAULT_UNIT_OF_MEASUREMENT: Final = "quakes" + +FEED: Final = "feed" + +IMAGE_URL_PATTERN: Final = "http://shakemap.rm.ingv.it/shake4/data/{}/current/products/intensity.jpg" + +PLATFORMS: Final = [Platform.SENSOR, Platform.GEO_LOCATION] + +SOURCE: Final = "ingv_centro_nazionale_terremoti" + +VERSION: Final = __version__ diff --git a/custom_components/ingv_centro_nazionale_terremoti/geo_location.py b/custom_components/ingv_centro_nazionale_terremoti/geo_location.py old mode 100755 new mode 100644 index f7f8e6f..7fc24cc --- a/custom_components/ingv_centro_nazionale_terremoti/geo_location.py +++ b/custom_components/ingv_centro_nazionale_terremoti/geo_location.py @@ -1,269 +1,236 @@ -"""Support for INGV Centro Nazionale Terremoti (Earthquakes) Feeds.""" -from __future__ import annotations - -import logging -from datetime import timedelta - -import homeassistant.helpers.config_validation as cv -import voluptuous as vol -from georss_ingv_centro_nazionale_terremoti_client import ( - IngvCentroNazionaleTerremotiFeedManager, -) -from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_RADIUS, - CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import track_time_interval -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -ATTR_EXTERNAL_ID = "external_id" -ATTR_EVENT_ID = "event_id" -ATTR_IMAGE_URL = "image_url" -ATTR_MAGNITUDE = "magnitude" -ATTR_PUBLICATION_DATE = "publication_date" -ATTR_REGION = "region" -ATTR_TITLE = "title" - -CONF_MINIMUM_MAGNITUDE = "minimum_magnitude" - -DEFAULT_MINIMUM_MAGNITUDE = 0.0 -DEFAULT_RADIUS_IN_KM = 50.0 - -SCAN_INTERVAL = timedelta(minutes=5) - -SOURCE = "ingv_centro_nazionale_terremoti" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float), - vol.Optional( - CONF_MINIMUM_MAGNITUDE, default=DEFAULT_MINIMUM_MAGNITUDE - ): cv.positive_float, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the INGV Centro Nazionale Terremoti Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( - config.get(CONF_LATITUDE, hass.config.latitude), - config.get(CONF_LONGITUDE, hass.config.longitude), - ) - radius_in_km = config[CONF_RADIUS] - minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE] - # Initialize the entity manager. - feed = IngvCentroNazionaleTerremotiFeedEntityManager( - hass, add_entities, scan_interval, coordinates, radius_in_km, minimum_magnitude - ) - - def start_feed_manager(event): - """Start feed manager.""" - feed.startup() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) - - -class IngvCentroNazionaleTerremotiFeedEntityManager: - """Feed Entity Manager for INGV Centro Nazionale Terremoti GeoRSS feed.""" - - def __init__( - self, - hass, - add_entities, - scan_interval, - coordinates, - radius_in_km, - minimum_magnitude, - ): - """Initialize the Feed Entity Manager.""" - - self._hass = hass - self._feed_manager = IngvCentroNazionaleTerremotiFeedManager( - self._generate_entity, - self._update_entity, - self._remove_entity, - coordinates, - filter_radius=radius_in_km, - filter_minimum_magnitude=minimum_magnitude, - ) - self._add_entities = add_entities - self._scan_interval = scan_interval - - def startup(self): - """Start up this manager.""" - self._feed_manager.update() - self._init_regular_updates() - - def _init_regular_updates(self): - """Schedule regular updates at the specified interval.""" - track_time_interval( - self._hass, lambda now: self._feed_manager.update(), self._scan_interval - ) - - def get_entry(self, external_id): - """Get feed entry by external id.""" - return self._feed_manager.feed_entries.get(external_id) - - def _generate_entity(self, external_id): - """Generate new entity.""" - new_entity = IngvCentroNazionaleTerremotiLocationEvent(self, external_id) - # Add new entities to HA. - self._add_entities([new_entity], True) - - def _update_entity(self, external_id): - """Update entity.""" - dispatcher_send( - self._hass, f"ingv_centro_nazionale_terremoti_update_{external_id}" - ) - - def _remove_entity(self, external_id): - """Remove entity.""" - dispatcher_send( - self._hass, f"ingv_centro_nazionale_terremoti_delete_{external_id}" - ) - - -class IngvCentroNazionaleTerremotiLocationEvent(GeolocationEvent): - """This represents an external event with INGV Centro Nazionale Terremoti feed data.""" - - _attr_unit_of_measurement = LENGTH_KILOMETERS - - def __init__(self, feed_manager, external_id): - """Initialize entity with data from feed entry.""" - self._feed_manager = feed_manager - self._external_id = external_id - self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None - self._region = None - self._magnitude = None - self._publication_date = None - self._event_id = None - self._image_url = None - self._remove_signal_delete = None - self._remove_signal_update = None - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self._remove_signal_delete = async_dispatcher_connect( - self.hass, - f"ingv_centro_nazionale_terremoti_delete_{self._external_id}", - self._delete_callback, - ) - self._remove_signal_update = async_dispatcher_connect( - self.hass, - f"ingv_centro_nazionale_terremoti_update_{self._external_id}", - self._update_callback, - ) - - @callback - def _delete_callback(self): - """Remove this entity.""" - self._remove_signal_delete() - self._remove_signal_update() - self.hass.async_create_task(self.async_remove(force_remove=True)) - - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_ha_state(True) - - @property - def should_poll(self): - """No polling needed for INGV Centro Nazionale Terremoti feed location events.""" - return False - - async def async_update(self): - """Update this entity from the data held in the feed manager.""" - _LOGGER.debug("Updating %s", self._external_id) - feed_entry = self._feed_manager.get_entry(self._external_id) - if feed_entry: - self._update_from_feed(feed_entry) - - def _update_from_feed(self, feed_entry): - """Update the internal state from the provided feed entry.""" - self._title = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution - self._region = feed_entry.region - self._magnitude = feed_entry.magnitude - self._publication_date = feed_entry.published - self._event_id = feed_entry.event_id - self._image_url = feed_entry.image_url - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - if self._magnitude and self._region: - return f"M {self._magnitude:.1f} - {self._region}" - if self._magnitude: - return f"M {self._magnitude:.1f}" - if self._region: - return self._region - return self._title - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attributes = {} - for key, value in ( - (ATTR_EXTERNAL_ID, self._external_id), - (ATTR_TITLE, self._title), - (ATTR_REGION, self._region), - (ATTR_MAGNITUDE, self._magnitude), - (ATTR_ATTRIBUTION, self._attribution), - (ATTR_PUBLICATION_DATE, self._publication_date), - (ATTR_EVENT_ID, self._event_id), - (ATTR_IMAGE_URL, self._image_url), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes +"""Support for INGV Earthquakes integration geo location events.""" +from __future__ import annotations + +import functools +import logging + +import voluptuous as vol + +from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + ATTR_TIME, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + LENGTH_MILES, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from . import IngvDataUpdateCoordinator +from .const import ( + CONF_MINIMUM_MAGNITUDE, + DEFAULT_FORCE_UPDATE, + DEFAULT_MINIMUM_MAGNITUDE, + DEFAULT_RADIUS, + DOMAIN, + FEED, + IMAGE_URL_PATTERN, + SOURCE, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_DEPTH = "depth" +ATTR_EVENT_ID = "event_id" +ATTR_EXTERNAL_ID = "external_id" +ATTR_IMAGE_URL = "image_url" +ATTR_MAGNITUDE = "magnitude" +ATTR_MODE = "mode" +ATTR_PUBLICATION_DATE = "publication_date" +ATTR_REGION = "region" +ATTR_STATUS = "status" + +# Deprecated. +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional(CONF_MINIMUM_MAGNITUDE, default=DEFAULT_MINIMUM_MAGNITUDE): cv.positive_float, + } +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the INGV Earthquakes integration platform.""" + coordinator = hass.data[DOMAIN][FEED][entry.entry_id] + + @callback + def async_add_geolocation(coordinator, config_entry_unique_id, external_id): + """Add geolocation entity from feed.""" + new_entity = IngvGeolocationEvent(coordinator, config_entry_unique_id, external_id) + _LOGGER.debug("Adding geolocation %s", new_entity) + async_add_entities([new_entity], False) + + coordinator.listeners.append( + async_dispatcher_connect(hass, coordinator.async_event_new_entity(), async_add_geolocation) + ) + _LOGGER.debug("Geolocation setup done") + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the INGV Earthquakes integration platform.""" + _LOGGER.warning( + "Configuration of the INGV platform in YAML is deprecated and will be " + "removed in the next release;" + "Your existing configuration for %s " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file", + config.get("platform", DOMAIN), + ) + hass.async_create_task( + hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_IMPORT}, data=config) + ) + + +class IngvGeolocationEvent(CoordinatorEntity, GeolocationEvent): + """This represents an external event with INGV Earthquakes integration data.""" + + coordinator: IngvDataUpdateCoordinator + _attr_force_update = DEFAULT_FORCE_UPDATE + _attr_icon = "mdi:pulse" + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__( + self, + coordinator: IngvDataUpdateCoordinator, + config_entry_unique_id: str | None, + external_id: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._external_id = external_id + self._event_id = external_id.split("eventId=")[1] + self._attr_unique_id = f"{config_entry_unique_id}_{self._event_id}" + # I think I will rename the domain in the future. (INGV Earthquake?) + self.entity_id = f"geo_location.ingv_earthquakes_{self._attr_unique_id}" + self._description = None + self._distance: float | None = None + self._latitude: float | None = None + self._longitude: float | None = None + self._depth = None + self._region = None + self._magnitude = None + self._status = None + self._mode = None + self._time = None + self._image_url = None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.coordinator.entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) + + async def async_added_to_hass(self) -> None: + """Call when entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_delete_{self._external_id}", + functools.partial(self.async_remove, force_remove=True), + ) + ) + self._update_internal_state() + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + entity_registry = er.async_get(self.hass) + # Remove from entity registry. + if self.entity_id in entity_registry.entities: + entity_registry.async_remove(self.entity_id) + _LOGGER.debug("Removed geolocation %s from entity registry", self.entity_id) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.coordinator.entry_available(self._external_id) + + @property + def distance(self) -> float | None: + """Return distance value of this external event.""" + return self._distance + + def _update_internal_state(self) -> None: + """Update state and attributes from coordinator data.""" + _LOGGER.debug("Updating %s from coordinator data", self._external_id) + if feed_entry := self.coordinator.get_entry(self._external_id): + self._depth = round(feed_entry.origin.depth / 1000) + self._distance = feed_entry.distance_to_home + # Convert distance and depth if not metric system. + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._depth = IMPERIAL_SYSTEM.length(self._depth, LENGTH_KILOMETERS) + self._distance = IMPERIAL_SYSTEM.length(self._distance, LENGTH_KILOMETERS) + self._attr_unit_of_measurement = LENGTH_MILES + + self._description = feed_entry.description + self._latitude = feed_entry.coordinates[0] + self._longitude = feed_entry.coordinates[1] + self._magnitude = feed_entry.magnitude.mag + # extra attribute image url not in feed_entry + if self._magnitude >= 3: + self._image_url = IMAGE_URL_PATTERN.format(self._event_id) + self._region = feed_entry._quakeml_event.description.text + self._time = feed_entry.origin.time + self._status = feed_entry.origin.evaluation_status + self._mode = feed_entry.origin.evaluation_mode + + self._attr_attribution = feed_entry.attribution + self._attr_name = f"M {self._magnitude:.1f} - {self._region}" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_internal_state() + super()._handle_coordinator_update() + + @property + def latitude(self) -> float | None: + """Return latitude value of this external event.""" + return self._latitude + + @property + def longitude(self) -> float | None: + """Return longitude value of this external event.""" + return self._longitude + + @property + def source(self) -> str: + """Return source value of this external event.""" + return SOURCE + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_EVENT_ID, self._event_id), + (ATTR_DEPTH, self._depth), + (ATTR_REGION, self._region), + (ATTR_MAGNITUDE, self._magnitude), + (ATTR_STATUS, self._status), + (ATTR_MODE, self._mode), + (ATTR_PUBLICATION_DATE, self._time), + (ATTR_IMAGE_URL, self._image_url), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/custom_components/ingv_centro_nazionale_terremoti/manifest.json b/custom_components/ingv_centro_nazionale_terremoti/manifest.json index b94ba98..90143bf 100644 --- a/custom_components/ingv_centro_nazionale_terremoti/manifest.json +++ b/custom_components/ingv_centro_nazionale_terremoti/manifest.json @@ -1,10 +1,13 @@ { - "domain": "ingv_centro_nazionale_terremoti", - "iot_class": "cloud_polling", - "name": "INGV Centro Nazionale Terremoti", - "version": "2022.02.0", - "documentation": "https://github.com/exxamalte/python-georss-ingv-centro-nazionale-terremoti-client", - "requirements": ["georss-ingv-centro-nazionale-terremoti-client==0.6"], - "issue_tracker": "https://github.com/caiosweet/Home-Assistant-custom-components-INGV/issues", - "codeowners": ["@exxamalte", "@caiosweet"] + "domain": "ingv_centro_nazionale_terremoti", + "name": "INGV Earthquakes", + "config_flow": true, + "documentation": "https://github.com/caiosweet/Home-Assistant-custom-components-INGV", + "issue_tracker": "https://github.com/caiosweet/Home-Assistant-custom-components-INGV/issues", + "requirements": ["aio_quakeml_ingv_centro_nazionale_terremoti_client==0.2"], + "codeowners": ["@exxamalte", "@caiosweet"], + "quality_scale": "platinum", + "iot_class": "cloud_polling", + "loggers": ["aio_quakeml_ingv_centro_nazionale_terremoti_client"], + "version": "0.0.0" } diff --git a/custom_components/ingv_centro_nazionale_terremoti/sensor.py b/custom_components/ingv_centro_nazionale_terremoti/sensor.py new file mode 100644 index 0000000..b87c9cd --- /dev/null +++ b/custom_components/ingv_centro_nazionale_terremoti/sensor.py @@ -0,0 +1,120 @@ +"""INGV Earthquakes integration status sensor.""" +from __future__ import annotations + +import logging + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt + +from . import IngvDataUpdateCoordinator +from .const import ( + ATTR_CREATED, + ATTR_LAST_TIMESTAMP, + ATTR_LAST_UPDATE, + ATTR_LAST_UPDATE_SUCCESSFUL, + ATTR_REMOVED, + ATTR_STATUS, + ATTR_UPDATED, + DEFAULT_FORCE_UPDATE, + DEFAULT_UNIT_OF_MEASUREMENT, + DOMAIN, + FEED, + VERSION, +) + +_LOGGER = logging.getLogger(__name__) + + +# An update of this entity is not making a web request, but uses internal data only. +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the INGV Earthquakes integration sensor platform.""" + coordinator = hass.data[DOMAIN][FEED][entry.entry_id] + config_entry_unique_id = entry.unique_id + + async_add_entities( + [IngvSensorEntity(coordinator, config_entry_unique_id, entry.title)], + True, + ) + _LOGGER.debug("Sensor setup done") + + +class IngvSensorEntity(CoordinatorEntity, SensorEntity): + """Implementation of the sensor.""" + + coordinator: IngvDataUpdateCoordinator + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_force_update = DEFAULT_FORCE_UPDATE + _attr_native_unit_of_measurement = DEFAULT_UNIT_OF_MEASUREMENT + _attr_icon = "mdi:pulse" + + def __init__( + self, + coordinator: IngvDataUpdateCoordinator, + config_entry_unique_id: str | None, + config_title: str | None, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._config_title = config_title + self._attr_unique_id = f"{config_entry_unique_id}_status" + self._attr_name = f"Ingv Earthquakes {config_title} status" + # I think I will rename the domain in the future. (INGV Earthquakes?) + self.entity_id = f"sensor.ingv_earthquakes_{self._attr_unique_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + manufacturer="Istituto Nazionale di Geofisica e Vulcanologia", + name="INGV Earthquakes", + sw_version=VERSION, + ) + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._update_internal_state() + + def _update_internal_state(self) -> None: + """Update state and attributes from coordinator data.""" + + if status_info := self.coordinator.status_info(): + _LOGGER.debug("Updating state from %s", status_info) + self._attr_native_value = status_info.total + self._attr_extra_state_attributes = {} + for key, value in ( + (ATTR_STATUS, status_info.status), + ( + ATTR_LAST_UPDATE, + (dt.as_utc(status_info.last_update) if status_info.last_update else None), + ), + ( + ATTR_LAST_UPDATE_SUCCESSFUL, + ( + dt.as_utc(status_info.last_update_successful) + if status_info.last_update_successful + else None + ), + ), + (ATTR_LAST_TIMESTAMP, status_info.last_timestamp), + (ATTR_CREATED, status_info.created), + (ATTR_UPDATED, status_info.updated), + (ATTR_REMOVED, status_info.removed), + ): + if value or isinstance(value, bool): + self._attr_extra_state_attributes[key] = value + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_internal_state() + super()._handle_coordinator_update() diff --git a/custom_components/ingv_centro_nazionale_terremoti/strings.json b/custom_components/ingv_centro_nazionale_terremoti/strings.json new file mode 100644 index 0000000..dd3a052 --- /dev/null +++ b/custom_components/ingv_centro_nazionale_terremoti/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user": { + "title": "Fill in your INGV details.", + "description": "Set up INGV Earthquakes integration", + "data": { + "location": "[%key:common::config_flow::data::location%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + }, + "options": { + "step": { + "init": { + "title": "INGV Options", + "description": "Set up INGV Earthquakes integration options", + "data": { + "minimum_magnitude": "Minimum magnitude", + "radius": "Radius", + "scan_interval": "Update interval (seconds)", + "start_time": "Start time delta (hours)" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/ingv_centro_nazionale_terremoti/translations/en.json b/custom_components/ingv_centro_nazionale_terremoti/translations/en.json new file mode 100644 index 0000000..db694cd --- /dev/null +++ b/custom_components/ingv_centro_nazionale_terremoti/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user": { + "title": "Fill in your INGV details.", + "description": "Set up INGV Earthquakes integration", + "data": { + "location": "Location", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "abort": { + "already_configured": "Service is already configured" + } + }, + "options": { + "step": { + "init": { + "title": "INGV Options", + "description": "Set up INGV Earthquakes integration options", + "data": { + "minimum_magnitude": "Minimum magnitude", + "radius": "Radius", + "scan_interval": "Update interval (Seconds)", + "start_time": "Start time delta (hours)" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/ingv_centro_nazionale_terremoti/translations/it.json b/custom_components/ingv_centro_nazionale_terremoti/translations/it.json new file mode 100644 index 0000000..d74f008 --- /dev/null +++ b/custom_components/ingv_centro_nazionale_terremoti/translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user": { + "title": "Inserisci i tuoi dati INGV.", + "description": "Configura l'istanza di INGV Terremoti", + "data": { + "location": "Localit\u00e0", + "latitude": "Latitudine", + "longitude": "Longitudine" + } + } + }, + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + } + }, + "options": { + "step": { + "init": { + "title": "Opzioni INGV", + "description": "Configura le opzioni INGV Terremoti", + "data": { + "minimum_magnitude": "Magnitudo minima", + "radius": "Raggio", + "scan_interval": "Intervallo di aggiornamento (secondi)", + "start_time": "Delta dell'ora di inizio (ore)" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/ingv_centro_nazionale_terremoti/version.py b/custom_components/ingv_centro_nazionale_terremoti/version.py new file mode 100644 index 0000000..40c3aa8 --- /dev/null +++ b/custom_components/ingv_centro_nazionale_terremoti/version.py @@ -0,0 +1 @@ +__version__ = "develop" diff --git a/hacs.json b/hacs.json index 67550ff..a3870fd 100644 --- a/hacs.json +++ b/hacs.json @@ -1,7 +1,8 @@ { - "name": "INGV Istituto Nazionale di Geofisica e Vulcanologia", - "domains": ["geo_location"], + "name": "INGV Earthquakes", + "zip_release": true, + "filename": "ingv_centro_nazionale_terremoti.zip", "country": ["IT"], "render_readme": true, - "iot_class": ["Cloud Polling"] + "homeassistant": "2022.6.0" }