meteocat 2.2.5 → 2.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/workflows/autocloser.yaml +25 -0
- package/.github/workflows/close-duplicates.yml +57 -0
- package/.github/workflows/publish-zip.yml +57 -0
- package/.github/workflows/stale.yml +63 -0
- package/CHANGELOG.md +22 -0
- package/custom_components/meteocat/__init__.py +29 -49
- package/custom_components/meteocat/config_flow.py +81 -176
- package/custom_components/meteocat/coordinator.py +12 -0
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/sensor.py +47 -8
- package/custom_components/meteocat/version.py +1 -1
- package/hacs.json +6 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "🐞 Bug report"
|
|
3
|
+
about: Report a problem with the integration
|
|
4
|
+
title: "[Bug] "
|
|
5
|
+
labels: bug
|
|
6
|
+
assignees: ""
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
**IMPORTANT: Please search the issues, including closed issues before opening a new issue.
|
|
10
|
+
The template is mandatory; failure to use it will result in issue closure.**
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
### Describe the bug
|
|
15
|
+
<!-- A clear and concise description of what the bug is. -->
|
|
16
|
+
|
|
17
|
+
### To Reproduce
|
|
18
|
+
<!-- Steps to reproduce the behavior. -->
|
|
19
|
+
1. Go to '...'
|
|
20
|
+
2. Click on '....'
|
|
21
|
+
3. Scroll down to '....'
|
|
22
|
+
4. See error
|
|
23
|
+
|
|
24
|
+
### Expected behavior
|
|
25
|
+
<!-- A clear and concise description of what you expected to happen. -->
|
|
26
|
+
|
|
27
|
+
### Screenshots
|
|
28
|
+
<!-- If applicable, add screenshots to help explain your problem. -->
|
|
29
|
+
|
|
30
|
+
### System details
|
|
31
|
+
- Home Assistant version:
|
|
32
|
+
- meteocat version (from `const.py` or HA startup log):
|
|
33
|
+
- meteocatpy version (from `pip show meteocatpy` in the HA container or HA startup log):
|
|
34
|
+
|
|
35
|
+
### Debug Logs (meteocat & meteocatpy)
|
|
36
|
+
<!-- Please provide [logs] - Enable debug logging for the component. -->
|
|
37
|
+
|
|
38
|
+
### Additional context
|
|
39
|
+
<!-- Add any other context about the problem here. -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Autocloser
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types: [opened, edited, reopened]
|
|
6
|
+
issue_comment:
|
|
7
|
+
types: [created]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
autoclose:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Autoclose issues that did not follow issue template
|
|
14
|
+
uses: roots/issue-closer@v1.1
|
|
15
|
+
with:
|
|
16
|
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
17
|
+
issue-close-message: >
|
|
18
|
+
👋 @${{ github.event.issue.user.login }} this issue was automatically closed because it did not follow the
|
|
19
|
+
[issue template](https://github.com/figorr/meteocat/issues/new/choose).
|
|
20
|
+
|
|
21
|
+
⚠️ Reminder:
|
|
22
|
+
**IMPORTANT: Please search the issues, including closed issues before opening a new issue.
|
|
23
|
+
The template is mandatory; failure to use it will result in issue closure.**
|
|
24
|
+
issue-pattern: >
|
|
25
|
+
(Describe the bug|To Reproduce|Expected behavior|System details|Debug Logs)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Close duplicate issues
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types:
|
|
6
|
+
- labeled
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
close-duplicate:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
if: github.event.label.name == 'duplicate'
|
|
12
|
+
steps:
|
|
13
|
+
- name: Extract linked issue number
|
|
14
|
+
id: extract
|
|
15
|
+
run: |
|
|
16
|
+
body="${{ github.event.issue.body }}"
|
|
17
|
+
# Convertimos a minúsculas para que el match sea insensible a mayúsculas
|
|
18
|
+
lower=$(echo "$body" | tr '[:upper:]' '[:lower:]')
|
|
19
|
+
|
|
20
|
+
# Buscamos "closed as duplicate of #123" o "duplicate of #123"
|
|
21
|
+
if [[ "$lower" =~ closed[[:space:]]+as[[:space:]]+duplicate[[:space:]]+of[[:space:]]+#([0-9]+) ]]; then
|
|
22
|
+
echo "number=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
|
|
23
|
+
elif [[ "$lower" =~ duplicate[[:space:]]+of[[:space:]]+#([0-9]+) ]]; then
|
|
24
|
+
echo "number=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
- name: Close issue
|
|
28
|
+
if: steps.extract.outputs.number != ''
|
|
29
|
+
uses: peter-evans/close-issue@v2
|
|
30
|
+
with:
|
|
31
|
+
issue-number: ${{ github.event.issue.number }}
|
|
32
|
+
comment: |
|
|
33
|
+
Closed as duplicate of #${{ steps.extract.outputs.number }}.
|
|
34
|
+
Please follow the discussion there.
|
|
35
|
+
|
|
36
|
+
- name: Update title
|
|
37
|
+
if: steps.extract.outputs.number != ''
|
|
38
|
+
uses: actions-ecosystem/action-edit-issue@v1
|
|
39
|
+
with:
|
|
40
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
41
|
+
issue_number: ${{ github.event.issue.number }}
|
|
42
|
+
title: "[Duplicate] ${{ github.event.issue.title }}"
|
|
43
|
+
|
|
44
|
+
- name: Remove duplicate label
|
|
45
|
+
if: steps.extract.outputs.number != ''
|
|
46
|
+
uses: actions-ecosystem/action-remove-labels@v1
|
|
47
|
+
with:
|
|
48
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
49
|
+
issue_number: ${{ github.event.issue.number }}
|
|
50
|
+
labels: duplicate
|
|
51
|
+
|
|
52
|
+
- name: Lock conversation
|
|
53
|
+
if: steps.extract.outputs.number != ''
|
|
54
|
+
uses: dessant/lock-threads@v4
|
|
55
|
+
with:
|
|
56
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
57
|
+
issue-lock-reason: "resolved"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Publish zip
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish-zip:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout repository
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
17
|
+
ref: master
|
|
18
|
+
|
|
19
|
+
- name: Get latest Home Assistant version
|
|
20
|
+
id: ha-version
|
|
21
|
+
run: |
|
|
22
|
+
latest=$(curl -s https://api.github.com/repos/home-assistant/core/releases/latest | jq -r .tag_name)
|
|
23
|
+
# quitamos prefijo "v" si existe
|
|
24
|
+
latest_clean=${latest#v}
|
|
25
|
+
echo "Latest HA version: $latest_clean"
|
|
26
|
+
echo "version=$latest_clean" >> $GITHUB_OUTPUT
|
|
27
|
+
|
|
28
|
+
- name: Update hacs.json
|
|
29
|
+
run: |
|
|
30
|
+
ha_version="${{ steps.ha-version.outputs.version }}"
|
|
31
|
+
echo "Updating hacs.json with HA version: $ha_version"
|
|
32
|
+
jq --arg ver "$ha_version" '.homeassistant = $ver' hacs.json > tmp.json && mv tmp.json hacs.json
|
|
33
|
+
cat hacs.json
|
|
34
|
+
|
|
35
|
+
- name: Commit updated hacs.json
|
|
36
|
+
run: |
|
|
37
|
+
git config user.name "github-actions[bot]"
|
|
38
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
39
|
+
git add hacs.json
|
|
40
|
+
if git commit -m "chore: update hacs.json with latest Home Assistant version"; then
|
|
41
|
+
git push origin master
|
|
42
|
+
else
|
|
43
|
+
echo "No changes to commit"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
- name: Zip integration
|
|
47
|
+
run: |
|
|
48
|
+
cd custom_components/meteocat
|
|
49
|
+
zip -r meteocat.zip ./
|
|
50
|
+
mv meteocat.zip $GITHUB_WORKSPACE/
|
|
51
|
+
|
|
52
|
+
- name: Upload meteocat.zip to release
|
|
53
|
+
uses: softprops/action-gh-release@v2.2.1
|
|
54
|
+
with:
|
|
55
|
+
files: meteocat.zip
|
|
56
|
+
env:
|
|
57
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Mark stale issues and pull requests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "30 1 * * *" # Ejecutar diariamente a la 01:30 UTC
|
|
6
|
+
workflow_dispatch: # Permite ejecución manual desde la interfaz
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
stale:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
issues: write
|
|
13
|
+
pull-requests: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
# 1. Issues/PRs generales sin actividad (excepto etiquetados)
|
|
17
|
+
- uses: actions/stale@v8
|
|
18
|
+
with:
|
|
19
|
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
20
|
+
days-before-stale: 60
|
|
21
|
+
days-before-close: 7
|
|
22
|
+
exempt-issue-labels: "help wanted,bug,feature"
|
|
23
|
+
exempt-pr-labels: "help wanted"
|
|
24
|
+
stale-issue-message: "This issue has had no activity for 60 days and will be closed in a week if there is no further activity."
|
|
25
|
+
stale-pr-message: "This pull request has had no activity for 60 days and will be closed in a week if there is no further activity."
|
|
26
|
+
stale-issue-label: "stale"
|
|
27
|
+
stale-pr-label: "stale"
|
|
28
|
+
|
|
29
|
+
# 2. Issues tipo "feature" sin actividad (solo etiquetar)
|
|
30
|
+
- uses: actions/stale@v8
|
|
31
|
+
with:
|
|
32
|
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
33
|
+
only-labels: "feature"
|
|
34
|
+
days-before-stale: 21
|
|
35
|
+
days-before-close: -1
|
|
36
|
+
stale-issue-message: >-
|
|
37
|
+
This feature request has had no activity for 3 weeks.
|
|
38
|
+
It has been marked as *help wanted* and will remain open.
|
|
39
|
+
stale-issue-label: "help wanted"
|
|
40
|
+
|
|
41
|
+
# 3. Issues tipo "bug" sin actividad (solo etiquetar)
|
|
42
|
+
- uses: actions/stale@v8
|
|
43
|
+
with:
|
|
44
|
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
45
|
+
only-labels: "bug"
|
|
46
|
+
days-before-stale: 21
|
|
47
|
+
days-before-close: -1
|
|
48
|
+
stale-issue-message: >-
|
|
49
|
+
This bug report has had no activity for 3 weeks.
|
|
50
|
+
It has been marked as *help wanted* and will remain open.
|
|
51
|
+
stale-issue-label: "help wanted"
|
|
52
|
+
|
|
53
|
+
# 4. Issues con "logs required" sin actividad (cerrar en 14 días)
|
|
54
|
+
- uses: actions/stale@v8
|
|
55
|
+
with:
|
|
56
|
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
57
|
+
only-labels: "logs required"
|
|
58
|
+
days-before-stale: 7
|
|
59
|
+
days-before-close: 7
|
|
60
|
+
stale-issue-message: >-
|
|
61
|
+
This issue has been marked as *requiring logs*, but no activity has been detected for 7 days.
|
|
62
|
+
Since logs were not provided, this issue will now be closed.
|
|
63
|
+
stale-issue-label: "stale"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## [2.2.7](https://github.com/figorr/meteocat/compare/v2.2.6...v2.2.7) (2025-08-29)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* 2.2.7 ([ece96b9](https://github.com/figorr/meteocat/commit/ece96b983e6ba5df8a2811fdb86452857cadbb18))
|
|
7
|
+
* fix HACS info ([ae829fa](https://github.com/figorr/meteocat/commit/ae829fa09047bd57d8f5aa8eae746bf31f3a30db))
|
|
8
|
+
* Fix meteor case sensitive for alerts sensors ([d247476](https://github.com/figorr/meteocat/commit/d24747660175d2c305fcffbbae6472ccd490cfb1))
|
|
9
|
+
* Fix umbral case insensitive ([73d6b58](https://github.com/figorr/meteocat/commit/73d6b5808acf8c896a7d822cab7640607f430b37))
|
|
10
|
+
* Fix warning log when an umbral is not in UMBRAL_MAPPING ([adf3511](https://github.com/figorr/meteocat/commit/adf351111e8cb14cba3fd2f2868496701b445b97))
|
|
11
|
+
* Include warning when umbral is not at UMBRAL_MAPPING ([c1b1f75](https://github.com/figorr/meteocat/commit/c1b1f75d7b6a219fbce4580a29e85e168127998e))
|
|
12
|
+
|
|
13
|
+
## [2.2.6](https://github.com/figorr/meteocat/compare/v2.2.5...v2.2.6) (2025-08-27)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* 2.2.6 ([bb92000](https://github.com/figorr/meteocat/commit/bb9200099ed951168bc891c870c6fd61b758c73d))
|
|
19
|
+
* Fix alerts region data update at first setup of the next entrances ([40018dc](https://github.com/figorr/meteocat/commit/40018dc0bb703a8dc606aaedcfa27b5c3e6bbf7a))
|
|
20
|
+
* Fix delete an entry but keeping common files for the rest of entries ([211e545](https://github.com/figorr/meteocat/commit/211e54510c458378ca46c465303995d1a9df0dbb))
|
|
21
|
+
* Fix region alerts json file for multiple entrances ([2b7072c](https://github.com/figorr/meteocat/commit/2b7072cb6fd7eafa1ab95dcedb2c2dea8f35005d))
|
|
22
|
+
|
|
1
23
|
## [2.2.5](https://github.com/figorr/meteocat/compare/v2.2.4...v2.2.5) (2025-02-16)
|
|
2
24
|
|
|
3
25
|
|
|
@@ -33,7 +33,7 @@ from .const import DOMAIN, PLATFORMS
|
|
|
33
33
|
_LOGGER = logging.getLogger(__name__)
|
|
34
34
|
|
|
35
35
|
# Versión
|
|
36
|
-
__version__ = "2.2.
|
|
36
|
+
__version__ = "2.2.7"
|
|
37
37
|
|
|
38
38
|
# Definir el esquema de configuración CONFIG_SCHEMA
|
|
39
39
|
CONFIG_SCHEMA = vol.Schema(
|
|
@@ -192,68 +192,48 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
|
192
192
|
"""Limpia cualquier dato adicional al desinstalar la integración."""
|
|
193
193
|
_LOGGER.info(f"Eliminando datos residuales de la integración: {entry.entry_id}")
|
|
194
194
|
|
|
195
|
-
# Definir las rutas base
|
|
195
|
+
# Definir las rutas base
|
|
196
196
|
custom_components_path = Path(hass.config.path("custom_components")) / DOMAIN
|
|
197
197
|
assets_folder = custom_components_path / "assets"
|
|
198
198
|
files_folder = custom_components_path / "files"
|
|
199
199
|
|
|
200
|
-
#
|
|
200
|
+
# Archivos comunes
|
|
201
201
|
symbols_file = assets_folder / "symbols.json"
|
|
202
202
|
variables_file = assets_folder / "variables.json"
|
|
203
|
+
alerts_file = files_folder / "alerts.json"
|
|
204
|
+
quotes_file = files_folder / "quotes.json"
|
|
203
205
|
|
|
204
|
-
#
|
|
206
|
+
# Archivos específicos de cada entry
|
|
205
207
|
station_id = entry.data.get("station_id")
|
|
206
|
-
if not station_id:
|
|
207
|
-
_LOGGER.warning("No se encontró 'station_id' en la configuración. No se puede eliminar el archivo de datos de la estación.")
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
# Archivo JSON de la estación
|
|
211
|
-
station_data_file = files_folder / f"station_{station_id.lower()}_data.json"
|
|
212
|
-
|
|
213
|
-
# Obtener el `town_id` para identificar el archivo a eliminar
|
|
214
208
|
town_id = entry.data.get("town_id")
|
|
215
|
-
if not town_id:
|
|
216
|
-
_LOGGER.warning("No se encontró 'town_id' en la configuración. No se puede eliminar el archivo de datos de la estación.")
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
# Archivo JSON UVI del municipio
|
|
220
|
-
town_data_file = files_folder / f"uvi_{town_id.lower()}_data.json"
|
|
221
|
-
|
|
222
|
-
# Arhivos JSON de las predicciones del municipio a eliminar
|
|
223
|
-
forecast_hourly_data_file = files_folder / f"forecast_{town_id.lower()}_hourly_data.json"
|
|
224
|
-
forecast_daily_data_file = files_folder / f"forecast_{town_id.lower()}_daily_data.json"
|
|
225
|
-
|
|
226
|
-
# Obtener el `region_id` para identificar el archivo a eliminar
|
|
227
209
|
region_id = entry.data.get("region_id")
|
|
228
|
-
if not region_id:
|
|
229
|
-
_LOGGER.warning("No se encontró 'region_id' en la configuración. No se puede eliminar el archivo de alertas de la comarca.")
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
# Archivos JSON de alertas
|
|
233
|
-
alerts_file = files_folder / "alerts.json"
|
|
234
|
-
alerts_region_file = files_folder / f"alerts_{region_id}.json"
|
|
235
|
-
|
|
236
|
-
# Archivo JSON de cuotas
|
|
237
|
-
quotes_file = files_folder / f"quotes.json"
|
|
238
210
|
|
|
239
|
-
# Archivo JSON de rayos
|
|
240
|
-
lightning_file = files_folder / f"lightning_{region_id}.json"
|
|
241
|
-
|
|
242
|
-
# Validar la ruta base
|
|
243
211
|
if not custom_components_path.exists():
|
|
244
212
|
_LOGGER.warning(f"La ruta {custom_components_path} no existe. No se realizará la limpieza.")
|
|
245
213
|
return
|
|
246
214
|
|
|
247
|
-
# Eliminar archivos
|
|
215
|
+
# Eliminar archivos específicos de la entrada
|
|
216
|
+
if station_id:
|
|
217
|
+
safe_remove(files_folder / f"station_{station_id.lower()}_data.json")
|
|
218
|
+
if town_id:
|
|
219
|
+
safe_remove(files_folder / f"uvi_{town_id.lower()}_data.json")
|
|
220
|
+
safe_remove(files_folder / f"forecast_{town_id.lower()}_hourly_data.json")
|
|
221
|
+
safe_remove(files_folder / f"forecast_{town_id.lower()}_daily_data.json")
|
|
222
|
+
if region_id:
|
|
223
|
+
safe_remove(files_folder / f"alerts_{region_id}.json")
|
|
224
|
+
safe_remove(files_folder / f"lightning_{region_id}.json")
|
|
225
|
+
|
|
226
|
+
# Siempre eliminables
|
|
248
227
|
safe_remove(symbols_file)
|
|
249
228
|
safe_remove(variables_file)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
229
|
+
|
|
230
|
+
# 🔑 Solo eliminar los archivos comunes si ya no quedan otras entradas
|
|
231
|
+
remaining_entries = [
|
|
232
|
+
e for e in hass.config_entries.async_entries(DOMAIN)
|
|
233
|
+
if e.entry_id != entry.entry_id
|
|
234
|
+
]
|
|
235
|
+
if not remaining_entries: # significa que estamos borrando la última
|
|
236
|
+
safe_remove(alerts_file)
|
|
237
|
+
safe_remove(quotes_file)
|
|
238
|
+
safe_remove(assets_folder, is_folder=True)
|
|
239
|
+
safe_remove(files_folder, is_folder=True)
|
|
@@ -10,14 +10,13 @@ from datetime import datetime, timezone
|
|
|
10
10
|
from zoneinfo import ZoneInfo
|
|
11
11
|
|
|
12
12
|
import voluptuous as vol
|
|
13
|
-
from aiohttp import ClientError
|
|
14
13
|
import aiofiles
|
|
15
14
|
import unicodedata
|
|
16
15
|
|
|
17
16
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
|
18
17
|
from homeassistant.core import callback
|
|
19
18
|
from homeassistant.exceptions import HomeAssistantError
|
|
20
|
-
from homeassistant.helpers import
|
|
19
|
+
from homeassistant.helpers import config_validation as cv
|
|
21
20
|
|
|
22
21
|
from .const import (
|
|
23
22
|
DOMAIN,
|
|
@@ -38,12 +37,12 @@ from .const import (
|
|
|
38
37
|
PROVINCE_NAME,
|
|
39
38
|
STATION_STATUS,
|
|
40
39
|
LIMIT_XEMA,
|
|
41
|
-
|
|
40
|
+
LIMIT_PREDICCIO,
|
|
42
41
|
LIMIT_XDDE,
|
|
43
42
|
LIMIT_BASIC,
|
|
44
43
|
LIMIT_QUOTA
|
|
45
44
|
)
|
|
46
|
-
|
|
45
|
+
|
|
47
46
|
from .options_flow import MeteocatOptionsFlowHandler
|
|
48
47
|
from meteocatpy.town import MeteocatTown
|
|
49
48
|
from meteocatpy.symbols import MeteocatSymbols
|
|
@@ -51,18 +50,22 @@ from meteocatpy.variables import MeteocatVariables
|
|
|
51
50
|
from meteocatpy.townstations import MeteocatTownStations
|
|
52
51
|
from meteocatpy.infostation import MeteocatInfoStation
|
|
53
52
|
from meteocatpy.quotes import MeteocatQuotes
|
|
54
|
-
|
|
55
53
|
from meteocatpy.exceptions import BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError
|
|
56
54
|
|
|
57
55
|
_LOGGER = logging.getLogger(__name__)
|
|
58
|
-
|
|
59
56
|
TIMEZONE = ZoneInfo("Europe/Madrid")
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
INITIAL_TEMPLATE = {
|
|
59
|
+
"actualitzat": {"dataUpdate": "1970-01-01T00:00:00+00:00"},
|
|
60
|
+
"dades": []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def normalize_name(name: str) -> str:
|
|
62
64
|
"""Normaliza el nombre eliminando acentos y convirtiendo a minúsculas."""
|
|
63
65
|
name = unicodedata.normalize("NFKD", name).encode("ASCII", "ignore").decode("utf-8")
|
|
64
66
|
return name.lower()
|
|
65
67
|
|
|
68
|
+
|
|
66
69
|
class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
67
70
|
"""Flujo de configuración para Meteocat."""
|
|
68
71
|
|
|
@@ -76,27 +79,25 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
76
79
|
self.station_id: str | None = None
|
|
77
80
|
self.station_name: str | None = None
|
|
78
81
|
self.region_id: str | None = None
|
|
79
|
-
self.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
self.region_name: str | None = None
|
|
83
|
+
self.province_id: str | None = None
|
|
84
|
+
self.province_name: str | None = None
|
|
85
|
+
self.station_type: str | None = None
|
|
86
|
+
self.latitude: float | None = None
|
|
87
|
+
self.longitude: float | None = None
|
|
88
|
+
self.altitude: float | None = None
|
|
89
|
+
self.station_status: str | None = None
|
|
90
|
+
|
|
91
|
+
async def fetch_and_save_quotes(self, api_key: str):
|
|
82
92
|
"""Obtiene las cuotas de la API de Meteocat y las guarda en quotes.json."""
|
|
83
93
|
meteocat_quotes = MeteocatQuotes(api_key)
|
|
84
|
-
quotes_dir = os.path.join(
|
|
85
|
-
self.hass.config.path(),
|
|
86
|
-
"custom_components",
|
|
87
|
-
"meteocat",
|
|
88
|
-
"files"
|
|
89
|
-
)
|
|
94
|
+
quotes_dir = os.path.join(self.hass.config.path(), "custom_components", "meteocat", "files")
|
|
90
95
|
os.makedirs(quotes_dir, exist_ok=True)
|
|
91
96
|
quotes_file = os.path.join(quotes_dir, "quotes.json")
|
|
92
97
|
|
|
93
98
|
try:
|
|
94
|
-
data = await asyncio.wait_for(
|
|
95
|
-
|
|
96
|
-
timeout=30
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# Modificar los nombres de los planes con normalización
|
|
99
|
+
data = await asyncio.wait_for(meteocat_quotes.get_quotes(), timeout=30)
|
|
100
|
+
|
|
100
101
|
plan_mapping = {
|
|
101
102
|
"xdde_": "XDDE",
|
|
102
103
|
"prediccio_": "Prediccio",
|
|
@@ -109,7 +110,6 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
109
110
|
for plan in data["plans"]:
|
|
110
111
|
normalized_nom = normalize_name(plan["nom"])
|
|
111
112
|
new_name = next((v for k, v in plan_mapping.items() if normalized_nom.startswith(k)), plan["nom"])
|
|
112
|
-
|
|
113
113
|
modified_plans.append({
|
|
114
114
|
"nom": new_name,
|
|
115
115
|
"periode": plan["periode"],
|
|
@@ -118,28 +118,23 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
118
118
|
"consultesRealitzades": plan["consultesRealitzades"]
|
|
119
119
|
})
|
|
120
120
|
|
|
121
|
-
# Añadir la clave 'actualitzat' con la fecha y hora actual de la zona horaria local
|
|
122
121
|
current_time = datetime.now(timezone.utc).astimezone(TIMEZONE).isoformat()
|
|
123
122
|
data_with_timestamp = {
|
|
124
|
-
"actualitzat": {
|
|
125
|
-
"dataUpdate": current_time
|
|
126
|
-
},
|
|
123
|
+
"actualitzat": {"dataUpdate": current_time},
|
|
127
124
|
"client": data["client"],
|
|
128
125
|
"plans": modified_plans
|
|
129
126
|
}
|
|
130
127
|
|
|
131
|
-
# Guardar los datos en el archivo JSON
|
|
132
128
|
async with aiofiles.open(quotes_file, "w", encoding="utf-8") as file:
|
|
133
129
|
await file.write(json.dumps(data_with_timestamp, ensure_ascii=False, indent=4))
|
|
134
|
-
|
|
135
130
|
_LOGGER.info("Cuotas guardadas exitosamente en %s", quotes_file)
|
|
136
131
|
|
|
137
132
|
except Exception as ex:
|
|
138
133
|
_LOGGER.error("Error al obtener o guardar las cuotas: %s", ex)
|
|
139
134
|
raise HomeAssistantError("No se pudieron obtener las cuotas de la API")
|
|
140
|
-
|
|
135
|
+
|
|
141
136
|
async def create_alerts_file(self):
|
|
142
|
-
"""Crea
|
|
137
|
+
"""Crea los archivos de alertas global y regional si no existen."""
|
|
143
138
|
alerts_dir = os.path.join(
|
|
144
139
|
self.hass.config.path(),
|
|
145
140
|
"custom_components",
|
|
@@ -147,67 +142,44 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
147
142
|
"files"
|
|
148
143
|
)
|
|
149
144
|
os.makedirs(alerts_dir, exist_ok=True)
|
|
150
|
-
alerts_file = os.path.join(alerts_dir, "alerts.json")
|
|
151
145
|
|
|
146
|
+
# Archivo global de alertas
|
|
147
|
+
alerts_file = os.path.join(alerts_dir, "alerts.json")
|
|
152
148
|
if not os.path.exists(alerts_file):
|
|
153
|
-
initial_data = {
|
|
154
|
-
"actualitzat": {
|
|
155
|
-
"dataUpdate": "1970-01-01T00:00:00+00:00"
|
|
156
|
-
},
|
|
157
|
-
"dades": []
|
|
158
|
-
}
|
|
159
149
|
async with aiofiles.open(alerts_file, "w", encoding="utf-8") as file:
|
|
160
|
-
await file.write(json.dumps(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
async with aiofiles.open(lightning_file, "w", encoding="utf-8") as file:
|
|
183
|
-
await file.write(json.dumps(initial_data, ensure_ascii=False, indent=4))
|
|
184
|
-
|
|
185
|
-
_LOGGER.info("Archivo %s creado", lightning_file)
|
|
186
|
-
|
|
187
|
-
async def async_step_user(
|
|
188
|
-
self, user_input: dict[str, Any] | None = None
|
|
189
|
-
) -> ConfigFlowResult:
|
|
190
|
-
"""Primer paso: Solicitar la API Key."""
|
|
150
|
+
await file.write(json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4))
|
|
151
|
+
_LOGGER.info("Archivo global %s creado con plantilla inicial", alerts_file)
|
|
152
|
+
|
|
153
|
+
# Solo si existe region_id
|
|
154
|
+
if self.region_id:
|
|
155
|
+
# Archivo regional de alertas
|
|
156
|
+
alerts_region_file = os.path.join(alerts_dir, f"alerts_{self.region_id}.json")
|
|
157
|
+
if not os.path.exists(alerts_region_file):
|
|
158
|
+
async with aiofiles.open(alerts_region_file, "w", encoding="utf-8") as file:
|
|
159
|
+
await file.write(json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4))
|
|
160
|
+
_LOGGER.info("Archivo regional %s creado con plantilla inicial", alerts_region_file)
|
|
161
|
+
|
|
162
|
+
# Archivo lightning regional
|
|
163
|
+
lightning_file = os.path.join(alerts_dir, f"lightning_{self.region_id}.json")
|
|
164
|
+
if not os.path.exists(lightning_file):
|
|
165
|
+
async with aiofiles.open(lightning_file, "w", encoding="utf-8") as file:
|
|
166
|
+
await file.write(json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4))
|
|
167
|
+
_LOGGER.info("Archivo lightning %s creado con plantilla inicial", lightning_file)
|
|
168
|
+
|
|
169
|
+
async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
|
170
|
+
"""Primer paso: solicitar API Key."""
|
|
191
171
|
errors = {}
|
|
192
|
-
|
|
193
172
|
if user_input is not None:
|
|
194
173
|
self.api_key = user_input[CONF_API_KEY]
|
|
195
|
-
|
|
196
174
|
town_client = MeteocatTown(self.api_key)
|
|
197
|
-
|
|
198
175
|
try:
|
|
199
176
|
self.municipis = await town_client.get_municipis()
|
|
200
|
-
# Aquí obtenemos y guardamos las cuotas
|
|
201
177
|
await self.fetch_and_save_quotes(self.api_key)
|
|
202
|
-
#
|
|
178
|
+
# Crea solo el archivo global de alertas (regional se hará después)
|
|
203
179
|
await self.create_alerts_file()
|
|
204
|
-
except
|
|
180
|
+
except Exception as ex:
|
|
205
181
|
_LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
|
|
206
182
|
errors["base"] = "cannot_connect"
|
|
207
|
-
except Exception as ex:
|
|
208
|
-
_LOGGER.error("Error inesperado al validar la API Key: %s", ex)
|
|
209
|
-
errors["base"] = "unknown"
|
|
210
|
-
|
|
211
183
|
if not errors:
|
|
212
184
|
return await self.async_step_select_municipi()
|
|
213
185
|
|
|
@@ -215,92 +187,44 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
215
187
|
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
|
216
188
|
|
|
217
189
|
async def async_step_select_municipi(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
|
218
|
-
"""Segundo paso:
|
|
190
|
+
"""Segundo paso: seleccionar el municipio."""
|
|
219
191
|
errors = {}
|
|
220
|
-
|
|
221
192
|
if user_input is not None:
|
|
222
193
|
selected_codi = user_input["municipi"]
|
|
223
|
-
self.selected_municipi = next(
|
|
224
|
-
(m for m in self.municipis if m["codi"] == selected_codi), None
|
|
225
|
-
)
|
|
226
|
-
|
|
194
|
+
self.selected_municipi = next((m for m in self.municipis if m["codi"] == selected_codi), None)
|
|
227
195
|
if self.selected_municipi:
|
|
228
196
|
await self.fetch_symbols_and_variables()
|
|
229
197
|
|
|
230
|
-
if
|
|
198
|
+
if self.selected_municipi:
|
|
231
199
|
return await self.async_step_select_station()
|
|
232
200
|
|
|
233
|
-
schema = vol.Schema(
|
|
234
|
-
{vol.Required("municipi"): vol.In({m["codi"]: m["nom"] for m in self.municipis})}
|
|
235
|
-
)
|
|
236
|
-
|
|
201
|
+
schema = vol.Schema({vol.Required("municipi"): vol.In({m["codi"]: m["nom"] for m in self.municipis})})
|
|
237
202
|
return self.async_show_form(step_id="select_municipi", data_schema=schema, errors=errors)
|
|
238
203
|
|
|
239
204
|
async def fetch_symbols_and_variables(self):
|
|
240
205
|
"""Descarga y guarda los símbolos y variables después de seleccionar el municipio."""
|
|
241
|
-
|
|
242
|
-
errors = {}
|
|
243
|
-
|
|
244
|
-
# Crear directorio de activos (assets) si no existe
|
|
245
|
-
assets_dir = os.path.join(
|
|
246
|
-
self.hass.config.path(),
|
|
247
|
-
"custom_components",
|
|
248
|
-
"meteocat",
|
|
249
|
-
"assets"
|
|
250
|
-
)
|
|
206
|
+
assets_dir = os.path.join(self.hass.config.path(), "custom_components", "meteocat", "assets")
|
|
251
207
|
os.makedirs(assets_dir, exist_ok=True)
|
|
252
|
-
|
|
253
|
-
# Rutas para los archivos de símbolos y variables
|
|
254
208
|
symbols_file = os.path.join(assets_dir, "symbols.json")
|
|
255
209
|
variables_file = os.path.join(assets_dir, "variables.json")
|
|
256
|
-
|
|
257
210
|
try:
|
|
258
|
-
|
|
259
|
-
symbols_client = MeteocatSymbols(self.api_key)
|
|
260
|
-
symbols_data = await symbols_client.fetch_symbols()
|
|
261
|
-
|
|
211
|
+
symbols_data = await MeteocatSymbols(self.api_key).fetch_symbols()
|
|
262
212
|
async with aiofiles.open(symbols_file, "w", encoding="utf-8") as file:
|
|
263
213
|
await file.write(json.dumps({"symbols": symbols_data}, ensure_ascii=False, indent=4))
|
|
264
|
-
|
|
265
|
-
_LOGGER.info(f"Símbolos guardados en {symbols_file}")
|
|
266
|
-
|
|
267
|
-
# Descargar y guardar las variables
|
|
268
|
-
variables_client = MeteocatVariables(self.api_key)
|
|
269
|
-
variables_data = await variables_client.get_variables()
|
|
270
|
-
|
|
214
|
+
variables_data = await MeteocatVariables(self.api_key).get_variables()
|
|
271
215
|
async with aiofiles.open(variables_file, "w", encoding="utf-8") as file:
|
|
272
216
|
await file.write(json.dumps({"variables": variables_data}, ensure_ascii=False, indent=4))
|
|
273
|
-
|
|
274
|
-
_LOGGER.info(f"Variables guardadas en {variables_file}")
|
|
275
|
-
|
|
276
|
-
# Buscar la variable de temperatura
|
|
277
|
-
self.variable_id = next(
|
|
278
|
-
(v["codi"] for v in variables_data if v["nom"].lower() == "temperatura"), None
|
|
279
|
-
)
|
|
280
|
-
if not self.variable_id:
|
|
281
|
-
_LOGGER.error("No se encontró la variable 'Temperatura'")
|
|
282
|
-
errors["base"] = "variable_not_found"
|
|
283
|
-
except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
|
|
284
|
-
_LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
|
|
285
|
-
errors["base"] = "cannot_connect"
|
|
217
|
+
self.variable_id = next((v["codi"] for v in variables_data if v["nom"].lower() == "temperatura"), None)
|
|
286
218
|
except Exception as ex:
|
|
287
|
-
_LOGGER.error("Error
|
|
288
|
-
|
|
219
|
+
_LOGGER.error("Error al descargar símbolos o variables: %s", ex)
|
|
220
|
+
raise HomeAssistantError("No se pudieron obtener símbolos o variables")
|
|
289
221
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
async def async_step_select_station(
|
|
294
|
-
self, user_input: dict[str, Any] | None = None
|
|
295
|
-
) -> ConfigFlowResult:
|
|
296
|
-
"""Tercer paso: Seleccionar la estación para la variable seleccionada."""
|
|
222
|
+
async def async_step_select_station(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
|
223
|
+
"""Tercer paso: seleccionar estación."""
|
|
297
224
|
errors = {}
|
|
298
|
-
|
|
299
225
|
townstations_client = MeteocatTownStations(self.api_key)
|
|
300
226
|
try:
|
|
301
|
-
stations_data = await townstations_client.get_town_stations(
|
|
302
|
-
self.selected_municipi["codi"], self.variable_id
|
|
303
|
-
)
|
|
227
|
+
stations_data = await townstations_client.get_town_stations(self.selected_municipi["codi"], self.variable_id)
|
|
304
228
|
except Exception as ex:
|
|
305
229
|
_LOGGER.error("Error al obtener las estaciones: %s", ex)
|
|
306
230
|
errors["base"] = "stations_fetch_failed"
|
|
@@ -309,19 +233,15 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
309
233
|
if user_input is not None:
|
|
310
234
|
selected_station_codi = user_input["station"]
|
|
311
235
|
selected_station = next(
|
|
312
|
-
(station for station in stations_data[0]["variables"][0]["estacions"] if station["codi"] == selected_station_codi),
|
|
313
|
-
None
|
|
236
|
+
(station for station in stations_data[0]["variables"][0]["estacions"] if station["codi"] == selected_station_codi), None
|
|
314
237
|
)
|
|
315
|
-
|
|
316
238
|
if selected_station:
|
|
317
239
|
self.station_id = selected_station["codi"]
|
|
318
240
|
self.station_name = selected_station["nom"]
|
|
319
241
|
|
|
320
242
|
# Obtener metadatos de la estación
|
|
321
|
-
infostation_client = MeteocatInfoStation(self.api_key)
|
|
322
243
|
try:
|
|
323
|
-
station_metadata = await
|
|
324
|
-
# Extraer los valores necesarios de los metadatos
|
|
244
|
+
station_metadata = await MeteocatInfoStation(self.api_key).get_infostation(self.station_id)
|
|
325
245
|
self.station_type = station_metadata.get("tipus", "")
|
|
326
246
|
self.latitude = station_metadata.get("coordenades", {}).get("latitud", 0.0)
|
|
327
247
|
self.longitude = station_metadata.get("coordenades", {}).get("longitud", 0.0)
|
|
@@ -332,9 +252,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
332
252
|
self.province_name = station_metadata.get("provincia", {}).get("nom", "")
|
|
333
253
|
self.station_status = station_metadata.get("estats", [{}])[0].get("codi", "")
|
|
334
254
|
|
|
335
|
-
# Crear
|
|
336
|
-
await self.
|
|
337
|
-
|
|
255
|
+
# Crear archivos regionales de alertas y lightning
|
|
256
|
+
await self.create_alerts_file()
|
|
257
|
+
|
|
338
258
|
return await self.async_step_set_api_limits()
|
|
339
259
|
except Exception as ex:
|
|
340
260
|
_LOGGER.error("Error al obtener los metadatos de la estación: %s", ex)
|
|
@@ -342,22 +262,12 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
342
262
|
else:
|
|
343
263
|
errors["base"] = "station_not_found"
|
|
344
264
|
|
|
345
|
-
schema = vol.Schema(
|
|
346
|
-
|
|
347
|
-
vol.Required("station"): vol.In(
|
|
348
|
-
{station["codi"]: station["nom"] for station in stations_data[0]["variables"][0]["estacions"]}
|
|
349
|
-
)
|
|
350
|
-
}
|
|
351
|
-
)
|
|
265
|
+
schema = vol.Schema({vol.Required("station"): vol.In({station["codi"]: station["nom"] for station in stations_data[0]["variables"][0]["estacions"]})})
|
|
266
|
+
return self.async_show_form(step_id="select_station", data_schema=schema, errors=errors)
|
|
352
267
|
|
|
353
|
-
return self.async_show_form(
|
|
354
|
-
step_id="select_station", data_schema=schema, errors=errors
|
|
355
|
-
)
|
|
356
|
-
|
|
357
268
|
async def async_step_set_api_limits(self, user_input=None):
|
|
358
|
-
"""Cuarto paso:
|
|
269
|
+
"""Cuarto paso: límites de la API."""
|
|
359
270
|
errors = {}
|
|
360
|
-
|
|
361
271
|
if user_input is not None:
|
|
362
272
|
self.limit_xema = user_input.get(LIMIT_XEMA, 750)
|
|
363
273
|
self.limit_prediccio = user_input.get(LIMIT_PREDICCIO, 100)
|
|
@@ -388,22 +298,17 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
388
298
|
LIMIT_XDDE: self.limit_xdde,
|
|
389
299
|
LIMIT_QUOTA: self.limit_quota,
|
|
390
300
|
LIMIT_BASIC: self.limit_basic,
|
|
391
|
-
}
|
|
301
|
+
}
|
|
392
302
|
)
|
|
393
303
|
|
|
394
|
-
schema = vol.Schema(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
return self.async_show_form(
|
|
405
|
-
step_id="set_api_limits", data_schema=schema, errors=errors
|
|
406
|
-
)
|
|
304
|
+
schema = vol.Schema({
|
|
305
|
+
vol.Required(LIMIT_XEMA, default=750): cv.positive_int,
|
|
306
|
+
vol.Required(LIMIT_PREDICCIO, default=100): cv.positive_int,
|
|
307
|
+
vol.Required(LIMIT_XDDE, default=250): cv.positive_int,
|
|
308
|
+
vol.Required(LIMIT_QUOTA, default=300): cv.positive_int,
|
|
309
|
+
vol.Required(LIMIT_BASIC, default=2000): cv.positive_int,
|
|
310
|
+
})
|
|
311
|
+
return self.async_show_form(step_id="set_api_limits", data_schema=schema, errors=errors)
|
|
407
312
|
|
|
408
313
|
@staticmethod
|
|
409
314
|
@callback
|
|
@@ -1268,6 +1268,18 @@ class MeteocatAlertsCoordinator(DataUpdateCoordinator):
|
|
|
1268
1268
|
if now - last_update > validity_duration:
|
|
1269
1269
|
return await self._fetch_and_save_new_data()
|
|
1270
1270
|
else:
|
|
1271
|
+
# Comprobar si el archivo regional sigue con INITIAL_TEMPLATE o sin datos válidos
|
|
1272
|
+
region_data = await load_json_from_file(self.alerts_region_file)
|
|
1273
|
+
if (
|
|
1274
|
+
not region_data
|
|
1275
|
+
or region_data.get("actualitzat", {}).get("dataUpdate") in [None, "1970-01-01T00:00:00+00:00"]
|
|
1276
|
+
):
|
|
1277
|
+
_LOGGER.info(
|
|
1278
|
+
"El archivo regional %s sigue con plantilla inicial. Regenerando a partir de alerts.json",
|
|
1279
|
+
self.alerts_region_file,
|
|
1280
|
+
)
|
|
1281
|
+
await self._filter_alerts_by_region()
|
|
1282
|
+
|
|
1271
1283
|
# Devolver los datos del archivo existente
|
|
1272
1284
|
_LOGGER.debug("Usando datos existentes de alertas: %s", existing_data)
|
|
1273
1285
|
return {
|
|
@@ -1357,6 +1357,13 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1357
1357
|
# Assign entity_category if defined in the description
|
|
1358
1358
|
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
1359
1359
|
|
|
1360
|
+
def _map_meteor_case_insensitive(self, meteor: str) -> str:
|
|
1361
|
+
"""Busca el meteor en el mapping sin importar mayúsculas/minúsculas."""
|
|
1362
|
+
for key, value in self.METEOR_MAPPING.items():
|
|
1363
|
+
if key.lower() == meteor.lower():
|
|
1364
|
+
return value
|
|
1365
|
+
return "unknown"
|
|
1366
|
+
|
|
1360
1367
|
@property
|
|
1361
1368
|
def native_value(self):
|
|
1362
1369
|
"""Devuelve el número de alertas activas."""
|
|
@@ -1368,10 +1375,13 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1368
1375
|
meteor_details = self.coordinator.data.get("detalles", {}).get("meteor", {})
|
|
1369
1376
|
|
|
1370
1377
|
# Convertimos las claves al formato deseado usando el mapping
|
|
1371
|
-
attributes = {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1378
|
+
attributes = {}
|
|
1379
|
+
for i, meteor in enumerate(meteor_details.keys()):
|
|
1380
|
+
mapped_name = self._map_meteor_case_insensitive(meteor)
|
|
1381
|
+
if not mapped_name:
|
|
1382
|
+
_LOGGER.warning("Meteor desconocido sin mapeo: '%s'. Añadirlo a 'METEOR_MAPPING' del coordinador 'MeteocatAlertRegionSensor' si es necesario.", meteor)
|
|
1383
|
+
mapped_name = "unknown"
|
|
1384
|
+
attributes[f"alert_{i+1}"] = mapped_name
|
|
1375
1385
|
|
|
1376
1386
|
_LOGGER.info("Atributos traducidos del sensor: %s", attributes)
|
|
1377
1387
|
return attributes
|
|
@@ -1461,6 +1471,23 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1461
1471
|
self._attr_unique_id,
|
|
1462
1472
|
)
|
|
1463
1473
|
|
|
1474
|
+
def _get_meteor_data_case_insensitive(self, meteor_type: str) -> dict:
|
|
1475
|
+
"""Busca en los datos de meteor de forma case-insensitive."""
|
|
1476
|
+
meteor_data_dict = self.coordinator.data.get("detalles", {}).get("meteor", {})
|
|
1477
|
+
for key, value in meteor_data_dict.items():
|
|
1478
|
+
if key.lower() == meteor_type.lower():
|
|
1479
|
+
return value
|
|
1480
|
+
return {}
|
|
1481
|
+
|
|
1482
|
+
def _get_umbral_case_insensitive(self, umbral: str) -> str:
|
|
1483
|
+
"""Convierte un umbral a su clave interna usando case-insensitive."""
|
|
1484
|
+
if umbral is None:
|
|
1485
|
+
return "unknown"
|
|
1486
|
+
for key, value in self.UMBRAL_MAPPING.items():
|
|
1487
|
+
if key.lower() == umbral.lower():
|
|
1488
|
+
return value
|
|
1489
|
+
return "unknown"
|
|
1490
|
+
|
|
1464
1491
|
@property
|
|
1465
1492
|
def native_value(self):
|
|
1466
1493
|
"""Devuelve el estado de la alerta específica."""
|
|
@@ -1468,7 +1495,7 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1468
1495
|
if not meteor_type:
|
|
1469
1496
|
return "Desconocido"
|
|
1470
1497
|
|
|
1471
|
-
meteor_data = self.
|
|
1498
|
+
meteor_data = self._get_meteor_data_case_insensitive(meteor_type)
|
|
1472
1499
|
|
|
1473
1500
|
# Convertir estado para translation_key
|
|
1474
1501
|
estado_original = meteor_data.get("estado", "Tancat")
|
|
@@ -1479,15 +1506,27 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1479
1506
|
"""Devuelve los atributos específicos de la alerta."""
|
|
1480
1507
|
meteor_type = self.METEOR_MAPPING.get(self.entity_description.key)
|
|
1481
1508
|
if not meteor_type:
|
|
1482
|
-
|
|
1509
|
+
_LOGGER.warning(
|
|
1510
|
+
"Tipo de meteor desconocido para sensor %s: '%s'. Añadirlo a 'METEOR_MAPPING' del coordinador 'MeteocatAlertMeteorSensor' si es necesario.",
|
|
1511
|
+
self.entity_description.key,
|
|
1512
|
+
self.coordinator.data.get("detalles", {}).get("meteor", {}).keys(),
|
|
1513
|
+
)
|
|
1514
|
+
return "unknown"
|
|
1483
1515
|
|
|
1484
|
-
meteor_data = self.
|
|
1516
|
+
meteor_data = self._get_meteor_data_case_insensitive(meteor_type)
|
|
1485
1517
|
if not meteor_data:
|
|
1486
1518
|
return {}
|
|
1487
1519
|
|
|
1488
1520
|
# Convertir umbral para translation_key
|
|
1489
1521
|
umbral_original = meteor_data.get("umbral")
|
|
1490
|
-
umbral_convertido = self.
|
|
1522
|
+
umbral_convertido = self._get_umbral_case_insensitive(umbral_original)
|
|
1523
|
+
|
|
1524
|
+
if umbral_convertido == "unknown" and umbral_original is not None:
|
|
1525
|
+
_LOGGER.warning(
|
|
1526
|
+
"Umbral desconocido para sensor %s: '%s'. Añadirlo a 'UMBRAL_MAPPING' del coordinador 'MeteocatAlertMeteorSensor' si es necesario.",
|
|
1527
|
+
self.entity_description.key,
|
|
1528
|
+
umbral_original
|
|
1529
|
+
)
|
|
1491
1530
|
|
|
1492
1531
|
return {
|
|
1493
1532
|
"inicio": meteor_data.get("inicio"),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "2.2.
|
|
2
|
+
__version__ = "2.2.7"
|
package/hacs.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.7",
|
|
4
4
|
"description": "[](https://opensource.org/licenses/Apache-2.0)\r [](https://pypi.org/project/meteocat)\r [](https://gitlab.com/figorr/meteocat/commits/master)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|