From ba2a834a25a80b49ea0d6a5b8cc4fa3c62529f8d Mon Sep 17 00:00:00 2001 From: Kev Date: Wed, 10 Jan 2024 17:01:02 -0500 Subject: [PATCH] Revert "Merge branch 'test-ruff-fix' into master" This reverts commit 589eb8cb0d9cd40783b1e933792fd0c75995df70, reversing changes made to 885b667ddae7c95b798e906d6354ed7cc5ffb30c. --- .devcontainer.json | 42 -- .github/dependabot.yml | 3 - .github/workflows/hassfest.yaml | 2 +- .github/workflows/lint.yml | 33 -- .github/workflows/validate.yaml | 2 +- .ruff.toml | 67 ---- .vscode/tasks.json | 11 - CODE_OF_CONDUCT.md | 132 ------ CONTRIBUTING.md | 61 --- config/configuration.yaml | 8 - custom_components/pirateweather/__init__.py | 130 +++--- .../pirateweather/config_flow.py | 347 ++++++++-------- custom_components/pirateweather/const.py | 82 ++-- custom_components/pirateweather/sensor.py | 379 +++++++++--------- custom_components/pirateweather/weather.py | 212 +++++----- .../weather_update_coordinator.py | 62 +-- example.png | Bin 0 -> 27860 bytes requirements.txt | 4 - requirements_dev.txt | 1 + requirements_test.txt | 1 + scripts/develop | 20 - scripts/lint | 7 - scripts/setup | 7 - 23 files changed, 628 insertions(+), 985 deletions(-) delete mode 100644 .devcontainer.json delete mode 100644 .github/workflows/lint.yml delete mode 100644 .ruff.toml delete mode 100644 .vscode/tasks.json delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.md delete mode 100644 config/configuration.yaml create mode 100644 example.png delete mode 100644 requirements.txt create mode 100644 requirements_dev.txt create mode 100644 requirements_test.txt delete mode 100644 scripts/develop delete mode 100644 scripts/lint delete mode 100644 scripts/setup diff --git a/.devcontainer.json b/.devcontainer.json deleted file mode 100644 index c70f1f5..0000000 --- a/.devcontainer.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "ludeeus/integration_blueprint", - "image": "mcr.microsoft.com/devcontainers/python:3.11-bullseye", - "postCreateCommand": "scripts/setup", - "forwardPorts": [ - 8123 - ], - "portsAttributes": { - "8123": { - "label": "Home Assistant", - "onAutoForward": "notify" - } - }, - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python", - "github.vscode-pull-request-github", - "ryanluker.vscode-coverage-gutters", - "ms-python.vscode-pylance" - ], - "settings": { - "files.eol": "\n", - "editor.tabSize": 4, - "python.pythonPath": "/usr/bin/python3", - "python.analysis.autoSearchPaths": false, - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true - } - } - }, - "remoteUser": "vscode", - "features": { - "ghcr.io/devcontainers/features/rust:1": {} - } -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 04afab9..0c8b08c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,9 +10,6 @@ updates: schedule: interval: "daily" open-pull-requests-limit: 100 - ignore: - # Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json - - dependency-name: "homeassistant" - package-ecosystem: "github-actions" directory: "/" diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 940b673..2f750eb 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -10,5 +10,5 @@ jobs: validate: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v4.1.1" + - uses: "actions/checkout@v4" - uses: home-assistant/actions/hassfest@master \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 1ca9818..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: "Lint" - -on: - push: - pull_request: - -jobs: - ruff: - name: "Ruff" - runs-on: "ubuntu-latest" - steps: - - name: "Checkout the repository" - uses: "actions/checkout@v4.1.1" - - - name: "Set up Python" - uses: actions/setup-python@v5.0.0 - with: - python-version: "3.11" - cache: "pip" - - - name: "Install requirements" - run: python3 -m pip install -r requirements.txt - - - name: "Format" - run: python3 -m ruff format . - - - name: "Check" - run: python3 -m ruff check . --fix --exit-zero - - - name: "Auto Commit" - uses: stefanzweifel/git-auto-commit-action@v5.0.0 - with: - commit_message: 'style fixes by ruff' diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 63110b0..db6b2ac 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -10,7 +10,7 @@ jobs: validate: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v4.1.1" + - uses: "actions/checkout@v4" - name: HACS validation uses: "hacs/action@main" with: diff --git a/.ruff.toml b/.ruff.toml deleted file mode 100644 index efc104e..0000000 --- a/.ruff.toml +++ /dev/null @@ -1,67 +0,0 @@ -# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml - -target-version = "py310" - -select = [ - "B007", # Loop control variable {name} not used within loop body - "B014", # Exception handler with duplicate exception - "C", # complexity - "D", # docstrings - "E", # pycodestyle - "F", # pyflakes/autoflake - "ICN001", # import concentions; {name} should be imported as {asname} - "PGH004", # Use specific rule codes when using noqa - "PLC0414", # Useless import alias. Import alias does not rename original package. - "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass - "SIM117", # Merge with-statements that use the same scope - "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() - "SIM201", # Use {left} != {right} instead of not {left} == {right} - "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} - "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. - "SIM401", # Use get from dict with default instead of an if block - "T20", # flake8-print - "TRY004", # Prefer TypeError exception for invalid type - "RUF006", # Store a reference to the return value of asyncio.create_task - "UP", # pyupgrade - "W", # pycodestyle -] - -ignore = [ - "D202", # No blank lines allowed after function docstring - "D203", # 1 blank line required before class docstring - "D213", # Multi-line docstring summary should start at the second line - "D404", # First word of the docstring should not be This - "D406", # Section name should end with a newline - "D407", # Section name underlining - "D411", # Missing blank line before section - "E501", # line too long - "E731", # do not assign a lambda expression, use a def - - # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules - "W191", - "E111", - "E114", - "E117", - "D206", - "D300", - "Q000", - "Q001", - "Q002", - "Q003", - "COM812", - "COM819", - "ISC001", - "ISC002", - - # Disabled because ruff does not understand type of __all__ generated by a function - "PLE0605", -] - -[flake8-pytest-style] -fixture-parentheses = false - -[pyupgrade] -keep-runtime-typing = true - -[mccabe] -max-complexity = 25 \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 3aa1c50..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Run Home Assistant on port 8123", - "type": "shell", - "command": "scripts/develop", - "problemMatcher": [] - } - ] -} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 153d903..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,132 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall - community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or advances of - any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, - without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official email address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -mail@pirateweather.net. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 21666e7..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contribution guidelines - -Contributing to this project should be as easy and transparent as possible, whether it's: - -- Reporting a bug -- Discussing the current state of the code -- Submitting a fix -- Proposing new features - -## Github is used for everything - -Github is used to host code, to track issues and feature requests, as well as accept pull requests. - -Pull requests are the best way to propose changes to the codebase. - -1. Fork the repo and create your branch from `main`. -2. If you've changed something, update the documentation. -3. Make sure your code lints (using `scripts/lint`). -4. Test you contribution. -5. Issue that pull request! - -## Any contributions you make will be under the Apache-2.0 License - -In short, when you submit code changes, your submissions are understood to be under the same [Apache-2.0](https://github.com/Pirate-Weather/pirate-weather-ha?tab=Apache-2.0-1-ov-file#readme) that covers the project. Feel free to contact the maintainers if that's a concern. - -## Report bugs using Github's [issues](https://github.com/Pirate-Weather/pirate-weather-ha/issues) - -GitHub issues are used to track public bugs. -Report a bug by [opening a new issue](https://github.com/Pirate-Weather/pirate-weather-ha/issues/new/choose); it's that easy! - -## Write bug reports with detail, background, and sample code - -**Great Bug Reports** tend to have: - -- A quick summary and/or background -- Steps to reproduce - - Be specific! - - Give sample code if you can. -- What you expected would happen -- What actually happens -- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) - -People *love* thorough bug reports. I'm not even kidding. - -## Use a Consistent Coding Style - -Use [black](https://github.com/ambv/black) to make sure the code follows the style. - -## Test your code modification - -This custom component is based on [integration_blueprint template](https://github.com/ludeeus/integration_blueprint). - -It comes with development environment in a container, easy to launch -if you use Visual Studio Code. With this container you will have a stand alone -Home Assistant instance running and already configured with the included -[`configuration.yaml`](./config/configuration.yaml) -file. - -## License - -By contributing, you agree that your contributions will be licensed under its Apache-2.0 License. diff --git a/config/configuration.yaml b/config/configuration.yaml deleted file mode 100644 index 539bb19..0000000 --- a/config/configuration.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# https://www.home-assistant.io/integrations/default_config/ -default_config: - -# https://www.home-assistant.io/integrations/logger/ -logger: - default: info - logs: - custom_components.pirateweather: debug diff --git a/custom_components/pirateweather/__init__.py b/custom_components/pirateweather/__init__.py index 74d93bc..eb4d841 100644 --- a/custom_components/pirateweather/__init__.py +++ b/custom_components/pirateweather/__init__.py @@ -4,10 +4,14 @@ import logging from typing import Any +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import EntityRegistry from datetime import timedelta +import forecastio -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -23,23 +27,31 @@ from .const import ( CONF_LANGUAGE, + CONFIG_FLOW_VERSION, DOMAIN, + DEFAULT_FORECAST_MODE, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + FORECAST_MODES, PLATFORMS, UPDATE_LISTENER, CONF_UNITS, + DEFAULT_UNITS, + DEFAULT_NAME, + FORECASTS_HOURLY, + FORECASTS_DAILY, PW_PLATFORMS, - PW_PLATFORM, + PW_PLATFORM, + PW_PREVPLATFORM, PW_ROUND, ) -# from .weather_update_coordinator import WeatherUpdateCoordinator, DarkSkyData -from .weather_update_coordinator import WeatherUpdateCoordinator - CONF_FORECAST = "forecast" CONF_HOURLY_FORECAST = "hourly_forecast" +#from .weather_update_coordinator import WeatherUpdateCoordinator, DarkSkyData +from .weather_update_coordinator import WeatherUpdateCoordinator + _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Pirate Weather" @@ -59,45 +71,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: pw_entity_platform = _get_config_value(entry, PW_PLATFORM) pw_entity_rounding = _get_config_value(entry, PW_ROUND) pw_scan_Int = entry.data[CONF_SCAN_INTERVAL] - + + # Extract list of int from forecast days/ hours string if present - # _LOGGER.warning('forecast_days_type: ' + str(type(forecast_days))) - - # _LOGGER.warning(forecast_days) + #_LOGGER.warning('forecast_days_type: ' + str(type(forecast_days))) + + #_LOGGER.warning(forecast_days) if type(forecast_days) == str: - # If empty, set to none - if forecast_days == "" or forecast_days == "None": - forecast_days = None - else: - if forecast_days[0] == "[": - forecast_days = forecast_days[1:-1].split(",") - else: - forecast_days = forecast_days.split(",") - forecast_days = [int(i) for i in forecast_days] - + # If empty, set to none + if forecast_days == "" or forecast_days == "None": + forecast_days = None + else: + if forecast_days[0] == '[': + forecast_days = forecast_days[1:-1].split(",") + else: + forecast_days = forecast_days.split(",") + forecast_days = [int(i) for i in forecast_days] + if type(forecast_hours) == str: - # If empty, set to none - if forecast_hours == "" or forecast_hours == "None": - forecast_hours = None - else: - if forecast_hours[0] == "[": - forecast_hours = forecast_hours[1:-1].split(",") - else: - forecast_hours = forecast_hours.split(",") - forecast_hours = [int(i) for i in forecast_hours] - - unique_location = f"pw-{latitude}-{longitude}" - + # If empty, set to none + if forecast_hours == "" or forecast_hours == "None": + forecast_hours = None + else: + if forecast_hours[0] == '[': + forecast_hours = forecast_hours[1:-1].split(",") + else: + forecast_hours = forecast_hours.split(",") + forecast_hours = [int(i) for i in forecast_hours] + + unique_location = (f"pw-{latitude}-{longitude}") + hass.data.setdefault(DOMAIN, {}) # Create and link weather WeatherUpdateCoordinator - weather_coordinator = WeatherUpdateCoordinator( - api_key, latitude, longitude, timedelta(seconds=pw_scan_Int), hass - ) - hass.data[DOMAIN][unique_location] = weather_coordinator + weather_coordinator = WeatherUpdateCoordinator(api_key, latitude, longitude, timedelta(seconds=pw_scan_Int), hass) + hass.data[DOMAIN][unique_location] = weather_coordinator - # await weather_coordinator.async_refresh() + #await weather_coordinator.async_refresh() await weather_coordinator.async_config_entry_first_refresh() - + + hass.data[DOMAIN][entry.entry_id] = { ENTRY_NAME: name, ENTRY_WEATHER_COORDINATOR: weather_coordinator, @@ -115,17 +127,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } # If both platforms - if (PW_PLATFORMS[0] in pw_entity_platform) and ( - PW_PLATFORMS[1] in pw_entity_platform - ): - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - # If only sensor + if (PW_PLATFORMS[0] in pw_entity_platform) and (PW_PLATFORMS[1] in pw_entity_platform): + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # If only sensor elif PW_PLATFORMS[0] in pw_entity_platform: - await hass.config_entries.async_forward_entry_setup(entry, PLATFORMS[0]) + await hass.config_entries.async_forward_entry_setup(entry, PLATFORMS[0]) # If only weather elif PW_PLATFORMS[1] in pw_entity_platform: - await hass.config_entries.async_forward_entry_setup(entry, PLATFORMS[1]) - + await hass.config_entries.async_forward_entry_setup(entry, PLATFORMS[1]) + + update_listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener return True @@ -135,36 +146,33 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - + pw_entity_prevplatform = hass.data[DOMAIN][entry.entry_id][PW_PLATFORM] + # If both - if (PW_PLATFORMS[0] in pw_entity_prevplatform) and ( - PW_PLATFORMS[1] in pw_entity_prevplatform - ): - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if (PW_PLATFORMS[0] in pw_entity_prevplatform) and (PW_PLATFORMS[1] in pw_entity_prevplatform): + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # If only sensor elif PW_PLATFORMS[0] in pw_entity_prevplatform: - unload_ok = await hass.config_entries.async_unload_platforms( - entry, [PLATFORMS[0]] - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, [PLATFORMS[0]]) # If only Weather elif PW_PLATFORMS[1] in pw_entity_prevplatform: - unload_ok = await hass.config_entries.async_unload_platforms( - entry, [PLATFORMS[1]] - ) - - _LOGGER.info("Unloading Pirate Weather") - + unload_ok = await hass.config_entries.async_unload_platforms(entry, [PLATFORMS[1]]) + + _LOGGER.info('Unloading Pirate Weather') + if unload_ok: update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] update_listener() - + hass.data[DOMAIN].pop(entry.entry_id) - + + return unload_ok diff --git a/custom_components/pirateweather/config_flow.py b/custom_components/pirateweather/config_flow.py index 251deec..d949667 100644 --- a/custom_components/pirateweather/config_flow.py +++ b/custom_components/pirateweather/config_flow.py @@ -3,6 +3,8 @@ import logging from datetime import timedelta +import forecastio +from forecastio.models import Forecast import json import aiohttp @@ -31,6 +33,8 @@ LANGUAGES, CONF_UNITS, DEFAULT_UNITS, + FORECASTS_DAILY, + FORECASTS_HOURLY, ALL_CONDITIONS, PW_PLATFORMS, PW_PLATFORM, @@ -61,37 +65,40 @@ async def async_step_user(self, user_input=None): errors = {} schema = vol.Schema( - { - vol.Required(CONF_API_KEY): str, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, - vol.Optional( - CONF_LATITUDE, default=self.hass.config.latitude - ): cv.latitude, - vol.Optional( - CONF_LONGITUDE, default=self.hass.config.longitude - ): cv.longitude, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int, - vol.Required(PW_PLATFORM, default=[PW_PLATFORMS[1]]): cv.multi_select( - PW_PLATFORMS - ), - vol.Required(CONF_MODE, default=DEFAULT_FORECAST_MODE): vol.In( - FORECAST_MODES - ), - vol.Required(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In( - LANGUAGES - ), - vol.Optional(CONF_FORECAST, default=""): str, - vol.Optional(CONF_HOURLY_FORECAST, default=""): str, - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): cv.multi_select( - ALL_CONDITIONS - ), - vol.Optional(PW_ROUND, default="No"): vol.In(["Yes", "No"]), - vol.Optional(CONF_UNITS, default=DEFAULT_UNITS): vol.In( - ["si", "us", "ca", "uk"] - ), - } - ) - + { + vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): int, + vol.Required(PW_PLATFORM, default=[PW_PLATFORMS[1]]): cv.multi_select( + PW_PLATFORMS + ), + vol.Required(CONF_MODE, default=DEFAULT_FORECAST_MODE): vol.In( + FORECAST_MODES + ), + vol.Required(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In( + LANGUAGES + ), + vol.Optional(CONF_FORECAST, default=""): str, + vol.Optional(CONF_HOURLY_FORECAST, default=""): str, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): cv.multi_select( + ALL_CONDITIONS + ), + vol.Optional(PW_ROUND, default="No"): vol.In(["Yes", "No"] + ), + vol.Optional(CONF_UNITS, default=DEFAULT_UNITS): vol.In(["si", "us", "ca", "uk"] + ), + } + ) + + if user_input is not None: latitude = user_input[CONF_LATITUDE] longitude = user_input[CONF_LONGITUDE] @@ -100,57 +107,53 @@ async def async_step_user(self, user_input=None): forecastMode = user_input[CONF_MODE] forecastPlatform = user_input[PW_PLATFORM] entityNamee = user_input[CONF_NAME] - + + # Convert scan interval to timedelta if isinstance(user_input[CONF_SCAN_INTERVAL], str): - user_input[CONF_SCAN_INTERVAL] = cv.time_period_str( - user_input[CONF_SCAN_INTERVAL] - ) - + user_input[CONF_SCAN_INTERVAL] = cv.time_period_str(user_input[CONF_SCAN_INTERVAL]) + + # Convert scan interval to number of seconds if isinstance(user_input[CONF_SCAN_INTERVAL], timedelta): - user_input[CONF_SCAN_INTERVAL] = user_input[ - CONF_SCAN_INTERVAL - ].total_seconds() - - # Unique value includes the location and forcastHours/ forecastDays to seperate WeatherEntity/ Sensor - # await self.async_set_unique_id(f"pw-{latitude}-{longitude}-{forecastDays}-{forecastHours}-{forecastMode}-{entityNamee}") - await self.async_set_unique_id( - f"pw-{latitude}-{longitude}-{forecastPlatform}-{forecastMode}-{entityNamee}" - ) + user_input[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL].total_seconds() + + # Unique value includes the location and forcastHours/ forecastDays to seperate WeatherEntity/ Sensor +# await self.async_set_unique_id(f"pw-{latitude}-{longitude}-{forecastDays}-{forecastHours}-{forecastMode}-{entityNamee}") + await self.async_set_unique_id(f"pw-{latitude}-{longitude}-{forecastPlatform}-{forecastMode}-{entityNamee}") + self._abort_if_unique_id_configured() + try: - api_status = await _is_pw_api_online( - self.hass, user_input[CONF_API_KEY], latitude, longitude - ) - - if api_status == 403: - _LOGGER.warning( - "Pirate Weather Setup Error: Invalid API Key, Ensure that the subscribe button is clicked for this endpoint: https://pirateweather.net/apis/hv9nrw1tjg/Beta" - ) - errors[ - "base" - ] = "Invalid API Key, Ensure that the subscribe button is clicked for this endpoint: https://pirateweather.net/apis/hv9nrw1tjg/Beta" - + api_status = await _is_pw_api_online( + self.hass, user_input[CONF_API_KEY], latitude, longitude + ) + + if api_status == 403: + _LOGGER.warning("Pirate Weather Setup Error: Invalid API Key, Ensure that the subscribe button is clicked for this endpoint: https://pirateweather.net/apis/hv9nrw1tjg/Beta") + errors["base"] = "Invalid API Key, Ensure that the subscribe button is clicked for this endpoint: https://pirateweather.net/apis/hv9nrw1tjg/Beta" + except: - _LOGGER.warning("Pirate Weather Setup Error: HTTP Error: " + api_status) - errors["base"] = "API Error: " + api_status - + _LOGGER.warning("Pirate Weather Setup Error: HTTP Error: " + api_status) + errors["base"] = "API Error: " + api_status + if not errors: return self.async_create_entry( title=user_input[CONF_NAME], data=user_input ) else: _LOGGER.warning(errors) - + + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + async def async_step_import(self, import_input=None): """Set the config entry up from yaml.""" config = import_input.copy() - + if CONF_NAME not in config: config[CONF_NAME] = DEFAULT_NAME if CONF_LATITUDE not in config: @@ -160,25 +163,25 @@ async def async_step_import(self, import_input=None): if CONF_MODE not in config: config[CONF_MODE] = DEFAULT_FORECAST_MODE if CONF_LANGUAGE not in config: - config[CONF_LANGUAGE] = DEFAULT_LANGUAGE + config[CONF_LANGUAGE] = DEFAULT_LANGUAGE if CONF_UNITS not in config: config[CONF_UNITS] = DEFAULT_UNITS if CONF_MONITORED_CONDITIONS not in config: - config[CONF_MONITORED_CONDITIONS] = [] + config[CONF_MONITORED_CONDITIONS] = [] if CONF_FORECAST not in config: - config[CONF_FORECAST] = "" + config[CONF_FORECAST] = "" if CONF_HOURLY_FORECAST not in config: config[CONF_HOURLY_FORECAST] = "" if CONF_API_KEY not in config: - config[CONF_API_KEY] = None + config[CONF_API_KEY] = None if PW_PLATFORM not in config: - config[PW_PLATFORM] = None + config[PW_PLATFORM] = None if PW_PREVPLATFORM not in config: - config[PW_PREVPLATFORM] = None + config[PW_PREVPLATFORM] = None if PW_ROUND not in config: - config[PW_ROUND] = "No" + config[PW_ROUND] = "No" if CONF_SCAN_INTERVAL not in config: - config[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL + config[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL return await self.async_step_user(config) @@ -193,127 +196,109 @@ async def async_step_init(self, user_input=None): """Manage the options.""" if user_input is not None: entry = self.config_entry - - # if self.config_entry.options: + + #if self.config_entry.options: # user_input[PW_PREVPLATFORM] = self.config_entry.options[PW_PLATFORM] - # else: - # user_input[PW_PREVPLATFORM] = self.hass.data[DOMAIN][entry.entry_id][PW_PLATFORM] - # self.hass.data[DOMAIN][entry.entry_id][PW_PREVPLATFORM] = self.hass.data[DOMAIN][entry.entry_id][PW_PLATFORM] - # user_input[PW_PREVPLATFORM] = self.hass.data[DOMAIN][entry.entry_id][PW_PLATFORM] - # _LOGGER.warning('async_step_init_Options') + #else: + #user_input[PW_PREVPLATFORM] = self.hass.data[DOMAIN][entry.entry_id][PW_PLATFORM] + #self.hass.data[DOMAIN][entry.entry_id][PW_PREVPLATFORM] = self.hass.data[DOMAIN][entry.entry_id][PW_PLATFORM] + #user_input[PW_PREVPLATFORM] = self.hass.data[DOMAIN][entry.entry_id][PW_PLATFORM] + #_LOGGER.warning('async_step_init_Options') return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) - + return self.async_show_form( step_id="init", data_schema=vol.Schema( - { - vol.Optional( - CONF_NAME, - default=self.config_entry.options.get( - CONF_NAME, - self.config_entry.data.get(CONF_NAME, DEFAULT_NAME), - ), - ): str, - vol.Optional( - CONF_LATITUDE, - default=self.config_entry.options.get( - CONF_LATITUDE, - self.config_entry.data.get( - CONF_LATITUDE, self.hass.config.latitude - ), - ), - ): cv.latitude, - vol.Optional( - CONF_LONGITUDE, - default=self.config_entry.options.get( - CONF_LONGITUDE, - self.config_entry.data.get( - CONF_LONGITUDE, self.hass.config.longitude - ), - ), - ): cv.longitude, - vol.Required( + { + vol.Optional( + CONF_NAME, + default=self.config_entry.options.get( + CONF_NAME, + self.config_entry.data.get(CONF_NAME, DEFAULT_NAME), + ), + ): str, + vol.Optional( + CONF_LATITUDE, + default=self.config_entry.options.get( + CONF_LATITUDE, + self.config_entry.data.get(CONF_LATITUDE, self.hass.config.latitude), + ), + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, + default=self.config_entry.options.get( + CONF_LONGITUDE, + self.config_entry.data.get(CONF_LONGITUDE, self.hass.config.longitude), + ), + ): cv.longitude, + vol.Required( + PW_PLATFORM, + default=self.config_entry.options.get( PW_PLATFORM, - default=self.config_entry.options.get( - PW_PLATFORM, - self.config_entry.data.get(PW_PLATFORM, []), - ), - ): cv.multi_select(PW_PLATFORMS), - vol.Optional( + self.config_entry.data.get(PW_PLATFORM, []), + ), + ): cv.multi_select(PW_PLATFORMS), + vol.Optional( + CONF_MODE, + default=self.config_entry.options.get( CONF_MODE, - default=self.config_entry.options.get( - CONF_MODE, - self.config_entry.data.get( - CONF_MODE, DEFAULT_FORECAST_MODE - ), - ), - ): vol.In(FORECAST_MODES), - vol.Optional( + self.config_entry.data.get(CONF_MODE, DEFAULT_FORECAST_MODE), + ), + ): vol.In(FORECAST_MODES), + vol.Optional( + CONF_LANGUAGE, + default=self.config_entry.options.get( CONF_LANGUAGE, - default=self.config_entry.options.get( - CONF_LANGUAGE, - self.config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE), - ), - ): vol.In(LANGUAGES), - vol.Optional( - CONF_FORECAST, - default=str( - self.config_entry.options.get( - CONF_FORECAST, - self.config_entry.data.get(CONF_FORECAST, ""), - ), - ), - ): str, - vol.Optional( - CONF_HOURLY_FORECAST, - default=str( - self.config_entry.options.get( - CONF_HOURLY_FORECAST, - self.config_entry.data.get(CONF_HOURLY_FORECAST, ""), - ), - ), - ): str, - vol.Optional( - CONF_MONITORED_CONDITIONS, - default=self.config_entry.options.get( - CONF_MONITORED_CONDITIONS, - self.config_entry.data.get(CONF_MONITORED_CONDITIONS, []), - ), - ): cv.multi_select(ALL_CONDITIONS), - vol.Optional( - CONF_UNITS, - default=self.config_entry.options.get( - CONF_UNITS, - self.config_entry.data.get(CONF_UNITS, DEFAULT_UNITS), - ), - ): vol.In(["si", "us", "ca", "uk"]), - vol.Optional( - PW_ROUND, - default=self.config_entry.options.get( - PW_ROUND, - self.config_entry.data.get(PW_ROUND, "No"), - ), - ): vol.In(["Yes", "No"]), - } + self.config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE), + ), + ): vol.In(LANGUAGES), + vol.Optional( + CONF_FORECAST, + default=str(self.config_entry.options.get( + CONF_FORECAST, + self.config_entry.data.get(CONF_FORECAST, "")), + ), + ): str, + vol.Optional( + CONF_HOURLY_FORECAST, + default=str(self.config_entry.options.get( + CONF_HOURLY_FORECAST, + self.config_entry.data.get(CONF_HOURLY_FORECAST, "")), + ), + ): str, + vol.Optional( + CONF_MONITORED_CONDITIONS, + default=self.config_entry.options.get( + CONF_MONITORED_CONDITIONS, + self.config_entry.data.get(CONF_MONITORED_CONDITIONS, []), + ), + ): cv.multi_select(ALL_CONDITIONS), + vol.Optional( + CONF_UNITS, + default=self.config_entry.options.get( + CONF_UNITS, + self.config_entry.data.get(CONF_UNITS, DEFAULT_UNITS), + ), + ): vol.In(["si", "us", "ca", "uk"]), + vol.Optional( + PW_ROUND, + default=self.config_entry.options.get( + PW_ROUND, + self.config_entry.data.get(PW_ROUND, "No"), + ), + ): vol.In(["Yes", "No"]), + } ), ) - async def _is_pw_api_online(hass, api_key, lat, lon): - forecastString = ( - "https://api.pirateweather.net/forecast/" - + api_key - + "/" - + str(lat) - + "," - + str(lon) - ) - - async with aiohttp.ClientSession(raise_for_status=False) as session: + forecastString = "https://api.pirateweather.net/forecast/" + api_key + "/" + str(lat) + "," + str(lon) + + async with aiohttp.ClientSession(raise_for_status=False) as session: async with session.get(forecastString) as resp: - resptext = await resp.text() - jsonText = json.loads(resptext) - headers = resp.headers - status = resp.status - - return status + resptext = await resp.text() + jsonText = json.loads(resptext) + headers = resp.headers + status = resp.status + + return status diff --git a/custom_components/pirateweather/const.py b/custom_components/pirateweather/const.py index e887a07..2343240 100644 --- a/custom_components/pirateweather/const.py +++ b/custom_components/pirateweather/const.py @@ -1,5 +1,6 @@ """Consts for the OpenWeatherMap.""" from __future__ import annotations +from datetime import timedelta from homeassistant.components.sensor import ( SensorDeviceClass, @@ -75,7 +76,7 @@ PW_PLATFORMS = ["Sensor", "Weather"] PW_PLATFORM = "pw_platform" PW_PREVPLATFORM = "pw_prevplatform" -PW_ROUND = "pw_round" +PW_ROUND = "pw_round" ATTR_FORECAST_CLOUD_COVERAGE = "cloud_coverage" ATTR_FORECAST_HUMIDITY = "humidity" @@ -86,50 +87,49 @@ FORECAST_MODES = [ FORECAST_MODE_HOURLY, - FORECAST_MODE_DAILY, -] - - + FORECAST_MODE_DAILY, + ] + + DEFAULT_FORECAST_MODE = FORECAST_MODE_DAILY FORECASTS_HOURLY = "forecasts_hourly" FORECASTS_DAILY = "forecasts_daily" -ALL_CONDITIONS = { - "summary": "Summary", - "icon": "Icon", - "precip_type": "Precipitation Type", - "precip_intensity": "Precipitation Intensity", - "precip_probability": "Precipitation Probability", - "precip_accumulation": "Precipitation Accumulation", - "temperature": "Temperature", - "apparent_temperature": "Apparent Temperature", - "dew_point": "Dew Point", - "humidity": "Humidity", - "wind_speed": "Wind Speed", - "wind_gust": "Wind Gust", - "wind_bearing": "Wind Bearing", - "cloud_cover": "Cloud Cover", - "pressure": "Pressure", - "visibility": "Visibility", - "ozone": "Ozone", - "minutely_summary": "Minutely Summary", - "hourly_summary": "Hourly Summary", - "daily_summary": "Daily Summary", - "temperature_high": "Temperature High", - "temperature_low": "Temperature Low", - "apparent_temperature_high": "Apparent Temperature High", - "apparent_temperature_low": "Apparent Temperature Low", - "precip_intensity_max": "Precip Intensity Max", - "uv_index": "UV Index", - "moon_phase": "Moon Phase", - "sunrise_time": "Sunrise Time", - "sunset_time": "Sunset Time", - "nearest_storm_distance": "Nearest Storm Distance", - "nearest_storm_bearing": "Nearest Storm Bearing", - "alerts": "Alerts", - "time": "Time", -} +ALL_CONDITIONS = {'summary': 'Summary', + 'icon': 'Icon', + 'precip_type': 'Precipitation Type', + 'precip_intensity': 'Precipitation Intensity', + 'precip_probability': 'Precipitation Probability', + 'precip_accumulation': 'Precipitation Accumulation', + 'temperature': 'Temperature', + 'apparent_temperature': 'Apparent Temperature', + 'dew_point': 'Dew Point', + 'humidity': 'Humidity', + 'wind_speed': 'Wind Speed', + 'wind_gust': 'Wind Gust', + 'wind_bearing': 'Wind Bearing', + 'cloud_cover': 'Cloud Cover', + 'pressure': 'Pressure', + 'visibility': 'Visibility', + 'ozone': 'Ozone', + 'minutely_summary': 'Minutely Summary', + 'hourly_summary': 'Hourly Summary', + 'daily_summary': 'Daily Summary', + 'temperature_high': 'Temperature High', + 'temperature_low': 'Temperature Low', + 'apparent_temperature_high': 'Apparent Temperature High', + 'apparent_temperature_low': 'Apparent Temperature Low', + 'precip_intensity_max': 'Precip Intensity Max', + 'uv_index': 'UV Index', + 'moon_phase': 'Moon Phase', + 'sunrise_time': 'Sunrise Time', + 'sunset_time': 'Sunset Time', + 'nearest_storm_distance': 'Nearest Storm Distance', + 'nearest_storm_bearing': 'Nearest Storm Bearing', + 'alerts': 'Alerts', + 'time':'Time' + } LANGUAGES = [ "af", @@ -325,4 +325,4 @@ name="Cloud coverage", native_unit_of_measurement=PERCENTAGE, ), -) +) \ No newline at end of file diff --git a/custom_components/pirateweather/sensor.py b/custom_components/pirateweather/sensor.py index e427e8a..3b12ec0 100644 --- a/custom_components/pirateweather/sensor.py +++ b/custom_components/pirateweather/sensor.py @@ -1,27 +1,39 @@ """Support for PirateWeather (Dark Sky Compatable weather service.""" +from datetime import timedelta import logging from dataclasses import dataclass, field +import forecastio +from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import voluptuous as vol import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, - SensorStateClass, + SensorStateClass ) from typing import Literal, NamedTuple from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.util import dt as dt_util +from homeassistant.util.unit_system import METRIC_SYSTEM + +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -44,12 +56,24 @@ from .const import ( CONF_LANGUAGE, + CONFIG_FLOW_VERSION, + DEFAULT_FORECAST_MODE, DEFAULT_LANGUAGE, DEFAULT_NAME, DEFAULT_SCAN_INTERVAL, DOMAIN, + FORECAST_MODES, + LANGUAGES, CONF_UNITS, + DEFAULT_UNITS, + ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + FORECAST_MODES, + PLATFORMS, + UPDATE_LISTENER, + MANUFACTURER, + FORECASTS_HOURLY, + FORECASTS_DAILY, ALL_CONDITIONS, PW_PLATFORMS, PW_PLATFORM, @@ -57,6 +81,7 @@ PW_ROUND, ) +from homeassistant.util import Throttle from .weather_update_coordinator import WeatherUpdateCoordinator @@ -90,7 +115,6 @@ "uk2": "uk2_unit", } - @dataclass class PirateWeatherSensorEntityDescription(SensorEntityDescription): """Describes Pirate Weather sensor entity.""" @@ -101,7 +125,8 @@ class PirateWeatherSensorEntityDescription(SensorEntityDescription): uk_unit: str | None = None uk2_unit: str | None = None forecast_mode: list[str] = field(default_factory=list) - + + # Sensor Types SENSOR_TYPES: dict[str, PirateWeatherSensorEntityDescription] = { "summary": PirateWeatherSensorEntityDescription( @@ -461,14 +486,12 @@ class PirateWeatherSensorEntityDescription(SensorEntityDescription): ), } - class ConditionPicture(NamedTuple): """Entity picture and icon for condition.""" entity_picture: str icon: str - CONDITION_PICTURES: dict[str, ConditionPicture] = { "clear-day": ConditionPicture( entity_picture="/static/images/darksky/weather-sunny.svg", @@ -572,34 +595,37 @@ class ConditionPicture(NamedTuple): ALLOWED_UNITS = ["auto", "si", "us", "ca", "uk", "uk2"] ALERTS_ATTRS = ["time", "description", "expires", "severity", "uri", "regions", "title"] - + HOURS = [i for i in range(168)] -DAYS = [i for i in range(7)] - +DAYS = [i for i in range(7)] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNITS): vol.In(ALLOWED_UNITS), vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, vol.Inclusive( CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" ): cv.latitude, vol.Inclusive( CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" ): cv.longitude, - vol.Optional(PW_PLATFORM): cv.multi_select(PW_PLATFORMS), + vol.Optional(PW_PLATFORM): cv.multi_select( + PW_PLATFORMS + ), vol.Optional(PW_PREVPLATFORM): cv.string, vol.Optional(CONF_FORECAST): cv.multi_select(DAYS), vol.Optional(CONF_HOURLY_FORECAST): cv.multi_select(HOURS), vol.Optional(CONF_MONITORED_CONDITIONS, default=None): cv.multi_select( ALL_CONDITIONS - ), + ), } ) + async def async_setup_platform( hass: HomeAssistant, config_entry: ConfigEntry, @@ -615,121 +641,90 @@ async def async_setup_platform( # Define as a sensor platform config_entry[PW_PLATFORM] = [PW_PLATFORMS[0]] - + # Set as no rounding for compatability - config_entry[PW_ROUND] = "No" - + config_entry[PW_ROUND] = "No" + hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config_entry - ) + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data = config_entry + ) ) async def async_setup_entry( - hass: HomeAssistant, + hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback ) -> None: """Set up Pirate Weather sensor entities based on a config entry.""" - + + domain_data = hass.data[DOMAIN][config_entry.entry_id] - + name = domain_data[CONF_NAME] - api_key = domain_data[CONF_API_KEY] + api_key = domain_data[CONF_API_KEY] weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] conditions = domain_data[CONF_MONITORED_CONDITIONS] latitude = domain_data[CONF_LATITUDE] longitude = domain_data[CONF_LONGITUDE] - units = domain_data[CONF_UNITS] + units = domain_data[CONF_UNITS] forecast_days = domain_data[CONF_FORECAST] forecast_hours = domain_data[CONF_HOURLY_FORECAST] - + # Round Output - outputRound = domain_data[PW_ROUND] - + outputRound = domain_data[PW_ROUND] + sensors: list[PirateWeatherSensor] = [] - + + for condition in conditions: + unit_index = {"si": 1, "us": 2, "ca": 3, "uk": 4, "uk2": 5}.get( domain_data[CONF_UNITS], 1 ) - + # Save units for conversion later requestUnits = domain_data[CONF_UNITS] - + sensorDescription = SENSOR_TYPES[condition] - + if condition in DEPRECATED_SENSOR_TYPES: _LOGGER.warning("Monitored condition %s is deprecated", condition) - - if ( - not sensorDescription.forecast_mode - or "currently" in sensorDescription.forecast_mode - ): + + if not sensorDescription.forecast_mode or "currently" in sensorDescription.forecast_mode: unique_id = f"{config_entry.unique_id}-sensor-{condition}" - sensors.append( - PirateWeatherSensor( - weather_coordinator, - condition, - name, - unique_id, - forecast_day=None, - forecast_hour=None, - description=sensorDescription, - requestUnits=requestUnits, - outputRound=outputRound, - ) - ) - + sensors.append(PirateWeatherSensor(weather_coordinator, condition, name, unique_id, forecast_day=None, forecast_hour=None, description=sensorDescription, requestUnits=requestUnits, outputRound=outputRound)) + + if forecast_days is not None and "daily" in sensorDescription.forecast_mode: for forecast_day in forecast_days: - unique_id = ( - f"{config_entry.unique_id}-sensor-{condition}-daily-{forecast_day}" - ) + unique_id = f"{config_entry.unique_id}-sensor-{condition}-daily-{forecast_day}" sensors.append( PirateWeatherSensor( - weather_coordinator, - condition, - name, - unique_id, - forecast_day=int(forecast_day), - forecast_hour=None, - description=sensorDescription, - requestUnits=requestUnits, - outputRound=outputRound, + weather_coordinator, condition, name, unique_id, forecast_day=int(forecast_day), forecast_hour=None, description=sensorDescription, requestUnits=requestUnits, outputRound=outputRound ) ) if forecast_hours is not None and "hourly" in sensorDescription.forecast_mode: for forecast_h in forecast_hours: - unique_id = ( - f"{config_entry.unique_id}-sensor-{condition}-hourly-{forecast_h}" - ) + unique_id = f"{config_entry.unique_id}-sensor-{condition}-hourly-{forecast_h}" sensors.append( PirateWeatherSensor( - weather_coordinator, - condition, - name, - unique_id, - forecast_day=None, - forecast_hour=int(forecast_h), - description=sensorDescription, - requestUnits=requestUnits, - outputRound=outputRound, + weather_coordinator, condition, name, unique_id, forecast_day=None, forecast_hour=int(forecast_h), description=sensorDescription, requestUnits=requestUnits, outputRound=outputRound ) ) - + async_add_entities(sensors) class PirateWeatherSensor(SensorEntity): """Class for an PirateWeather sensor.""" - # _attr_should_poll = False + #_attr_should_poll = False _attr_attribution = ATTRIBUTION entity_description: PirateWeatherSensorEntityDescription - + def __init__( self, weather_coordinator: WeatherUpdateCoordinator, @@ -738,29 +733,29 @@ def __init__( unique_id, forecast_day: int, forecast_hour: int, - description: PirateWeatherSensorEntityDescription, + description: PirateWeatherSensorEntityDescription, requestUnits: str, - outputRound: str, + outputRound: str ) -> None: """Initialize the sensor.""" self.client_name = name - - description = description + + description=description self.entity_description = description - self.description = description - + self.description=description + self._weather_coordinator = weather_coordinator - + self._attr_unique_id = unique_id self._attr_name = name - - # self._attr_device_info = DeviceInfo( + + #self._attr_device_info = DeviceInfo( # entry_type=DeviceEntryType.SERVICE, # identifiers={(DOMAIN, unique_id)}, # manufacturer=MANUFACTURER, # name=DEFAULT_NAME, - # ) - + #) + self.forecast_day = forecast_day self.forecast_hour = forecast_hour self.requestUnits = requestUnits @@ -768,9 +763,11 @@ def __init__( self.type = condition self._icon = None self._alerts = None - + + + self._name = description.name - + @property def name(self): """Return the name of the sensor.""" @@ -779,7 +776,8 @@ def name(self): if self.forecast_hour is not None: return f"{self.client_name} {self._name} {self.forecast_hour}h" return f"{self.client_name} {self._name}" - + + @property def available(self) -> bool: """Return if weather data is available from PirateWeather.""" @@ -803,7 +801,7 @@ def native_unit_of_measurement(self): def unit_system(self): """Return the unit system of this entity.""" return self.requestUnits - + @property def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" @@ -821,6 +819,7 @@ def update_unit_of_measurement(self) -> None: self._attr_native_unit_of_measurement = getattr( self.entity_description, unit_key ) + @property def icon(self): @@ -829,7 +828,7 @@ def icon(self): return CONDITION_PICTURES[self._icon][1] return SENSOR_TYPES[self.type][6] - + @property def icon(self) -> str | None: """Icon to use in the frontend, if any.""" @@ -845,28 +844,29 @@ def icon(self) -> str | None: def extra_state_attributes(self): """Return the state attributes.""" if self.type == "alerts": - extraATTR = self._alerts - extraATTR[ATTR_ATTRIBUTION] = ATTRIBUTION - - return extraATTR + extraATTR = self._alerts + extraATTR[ATTR_ATTRIBUTION] = ATTRIBUTION + + return extraATTR else: - return {ATTR_ATTRIBUTION: ATTRIBUTION} + return {ATTR_ATTRIBUTION: ATTRIBUTION} + @property def native_value(self) -> StateType: - """Return the state of the device.""" + """Return the state of the device.""" lookup_type = convert_to_camel(self.type) - + self.update_unit_of_measurement() - - if self.type == "alerts": + + if self.type == "alerts": data = self._weather_coordinator.data.alerts() - + alerts = {} if data is None: self._alerts = alerts return data - + multiple_alerts = len(data) > 1 for i, alert in enumerate(data): for attr in ALERTS_ATTRS: @@ -875,36 +875,37 @@ def native_value(self) -> StateType: else: dkey = attr alertsAttr = getattr(alert, attr) - + # Convert time to string if isinstance(alertsAttr, int): - alertsAttr = template_helper.timestamp_local(alertsAttr) - + alertsAttr = template_helper.timestamp_local(alertsAttr) + alerts[dkey] = alertsAttr - + + self._alerts = alerts - native_val = len(data) - + native_val = len(data) + + elif self.type == "minutely_summary": - native_val = getattr( - self._weather_coordinator.data.minutely(), "summary", "" - ) - self._icon = getattr(self._weather_coordinator.data.minutely(), "icon", "") + native_val = getattr(self._weather_coordinator.data.minutely(),"summary", "") + self._icon = getattr(self._weather_coordinator.data.minutely(),"icon", "") elif self.type == "hourly_summary": - native_val = getattr(self._weather_coordinator.data.hourly(), "summary", "") - self._icon = getattr(self._weather_coordinator.data.hourly(), "icon", "") - + native_val = getattr(self._weather_coordinator.data.hourly(),"summary", "") + self._icon = getattr(self._weather_coordinator.data.hourly(),"icon", "") + elif self.forecast_hour is not None: hourly = self._weather_coordinator.data.hourly() if hasattr(hourly, "data"): native_val = self.get_state(hourly.data[self.forecast_hour].d) else: native_val = 0 - + elif self.type == "daily_summary": - native_val = getattr(self._weather_coordinator.data.daily(), "summary", "") - self._icon = getattr(self._weather_coordinator.data.daily(), "icon", "") - + native_val = getattr(self._weather_coordinator.data.daily(),"summary", "") + self._icon = getattr(self._weather_coordinator.data.daily(),"icon", "") + + elif self.forecast_day is not None: daily = self._weather_coordinator.data.daily() if hasattr(daily, "data"): @@ -914,19 +915,21 @@ def native_value(self) -> StateType: else: currently = self._weather_coordinator.data.currently() native_val = self.get_state(currently.d) - - # self._state = native_val + + #self._state = native_val return native_val + def get_state(self, data): - """Return a new state based on the type. + """ + Return a new state based on the type. If the sensor type is unknown, the current state is returned. """ lookup_type = convert_to_camel(self.type) state = data.get(lookup_type) - + if state is None: return state @@ -935,70 +938,74 @@ def get_state(self, data): # If output rounding is requested, round to nearest integer if self.outputRound == "Yes": - roundingVal = 0 + roundingVal = 0 else: - roundingVal = 1 + roundingVal = 1 # Some state data needs to be rounded to whole values or converted to # percentages if self.type in ["precip_probability", "cloud_cover", "humidity"]: if roundingVal == 0: - state = int(round(state * 100, roundingVal)) + state = int(round(state * 100, roundingVal)) else: - state = round(state * 100, roundingVal) - + state = round(state * 100, roundingVal) + + # Logic to convert from SI to requsested units for compatability # Temps in F if self.requestUnits in ["us"]: - if self.type in [ - "dew_point", - "temperature", - "apparent_temperature", - "temperature_high", - "temperature_low", - "apparent_temperature_high", - "apparent_temperature_low", - ]: - state = (state * 9 / 5) + 32 - - # Precipitation Accumilation (mm in SI) to inches + if self.type in [ + "dew_point", + "temperature", + "apparent_temperature", + "temperature_high", + "temperature_low", + "apparent_temperature_high", + "apparent_temperature_low", + ]: + state = ((state * 9 / 5) + 32) + + # Precipitation Accumilation (mm in SI) to inches if self.requestUnits in ["us"]: - if self.type in [ - "precip_accumulation", - ]: - state = state * 0.0393701 - - # Precipitation Intensity (mm/h in SI) to inches + if self.type in [ + "precip_accumulation", + ]: + state = (state * 0.0393701) + + # Precipitation Intensity (mm/h in SI) to inches if self.requestUnits in ["us"]: - if self.type in [ - "precip_intensity", - ]: - state = state * 0.0393701 - - # Km to Miles + if self.type in [ + "precip_intensity", + ]: + state = (state * 0.0393701) + + + # Km to Miles if self.requestUnits in ["us", "uk", "uk2"]: - if self.type in [ - "visibility", - "nearest_storm_distance", - ]: - state = state * 0.621371 - - # Meters/second to Miles/hour + if self.type in [ + "visibility", + "nearest_storm_distance", + ]: + state = (state * 0.621371) + + # Meters/second to Miles/hour if self.requestUnits in ["us", "uk", "uk2"]: - if self.type in [ - "wind_speed", - "wind_gust", - ]: - state = state * 2.23694 - - # Meters/second to Km/ hour + if self.type in [ + "wind_speed", + "wind_gust", + ]: + state = (state * 2.23694) + + # Meters/second to Km/ hour if self.requestUnits in ["ca"]: - if self.type in [ - "wind_speed", - "wind_gust", - ]: - state = state * 3.6 - + if self.type in [ + "wind_speed", + "wind_gust", + ]: + state = (state * 3.6) + + + if self.type in [ "dew_point", "temperature", @@ -1018,14 +1025,15 @@ def get_state(self, data): "wind_speed", "wind_gust", ]: + if roundingVal == 0: - outState = int(round(state, roundingVal)) + outState = int(round(state, roundingVal)) else: - outState = round(state, roundingVal) - + outState = round(state, roundingVal) + else: - outState = state - + outState = state + return outState async def async_added_to_hass(self) -> None: @@ -1033,17 +1041,22 @@ async def async_added_to_hass(self) -> None: self.async_on_remove( self._weather_coordinator.async_add_listener(self.async_write_ha_state) ) - - # async def async_update(self) -> None: + + + #async def async_update(self) -> None: # """Get the latest data from PW and updates the states.""" - # await self._weather_coordinator.async_request_refresh() + # await self._weather_coordinator.async_request_refresh() + def convert_to_camel(data): - """Convert snake case (foo_bar_bat) to camel case (fooBarBat). + """ + Convert snake case (foo_bar_bat) to camel case (fooBarBat). This is not pythonic, but needed for certain situations. """ components = data.split("_") capital_components = "".join(x.title() for x in components[1:]) return f"{components[0]}{capital_components}" + + diff --git a/custom_components/pirateweather/weather.py b/custom_components/pirateweather/weather.py index d12cee4..8dfe5bf 100644 --- a/custom_components/pirateweather/weather.py +++ b/custom_components/pirateweather/weather.py @@ -1,16 +1,21 @@ """Support for the Pirate Weather (PW) service.""" from __future__ import annotations +from datetime import timedelta import logging +from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle from homeassistant.util.dt import utc_from_timestamp from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.typing import ConfigType from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant, callback -from .weather_update_coordinator import WeatherUpdateCoordinator from homeassistant.components.weather import ( @@ -25,7 +30,18 @@ ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, PLATFORM_SCHEMA, + WeatherEntity, Forecast, WeatherEntityFeature, SingleCoordinatorWeatherEntity, @@ -47,18 +63,35 @@ ) from .const import ( + CONF_LANGUAGE, + CONFIG_FLOW_VERSION, + DEFAULT_FORECAST_MODE, + DEFAULT_LANGUAGE, DEFAULT_NAME, DEFAULT_SCAN_INTERVAL, DOMAIN, FORECAST_MODES, + LANGUAGES, CONF_UNITS, + DEFAULT_UNITS, + ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + FORECAST_MODES, + PLATFORMS, + UPDATE_LISTENER, + MANUFACTURER, + FORECASTS_HOURLY, + FORECASTS_DAILY, PW_PLATFORMS, PW_PLATFORM, PW_PREVPLATFORM, PW_ROUND, + ATTR_FORECAST_CLOUD_COVERAGE, + ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_NATIVE_VISIBILITY, ) + _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Pirate Weather" @@ -73,7 +106,7 @@ vol.Optional(CONF_MODE, default="hourly"): vol.In(FORECAST_MODES), vol.Optional(CONF_UNITS): vol.In(["auto", "si", "us", "ca", "uk", "uk2"]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, } ) @@ -97,6 +130,7 @@ DEFAULT_NAME = "Pirate Weather" +from .weather_update_coordinator import WeatherUpdateCoordinator async def async_setup_platform( hass: HomeAssistant, @@ -110,17 +144,18 @@ async def async_setup_platform( "Your existing configuration has been imported into the UI automatically " "and can be safely removed from your configuration.yaml file" ) - + + # Add source to config config_entry[PW_PLATFORM] = [PW_PLATFORMS[1]] # Set as no rounding for compatability - config_entry[PW_ROUND] = "No" - + config_entry[PW_ROUND] = "No" + hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config_entry - ) + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data = config_entry + ) ) @@ -130,36 +165,31 @@ def _map_daily_forecast(forecast) -> Forecast: "condition": MAP_CONDITION.get(forecast.d.get("icon")), "native_temperature": forecast.d.get("temperatureHigh"), "native_templow": forecast.d.get("temperatureLow"), - "native_precipitation": forecast.d.get("precipAccumulation") * 10, - "precipitation_probability": round( - forecast.d.get("precipProbability") * 100, 0 - ), - "humidity": round(forecast.d.get("humidity") * 100, 2), - "cloud_coverage": round(forecast.d.get("cloudCover") * 100, 0), + "native_precipitation": forecast.d.get("precipAccumulation")*10, + "precipitation_probability": round(forecast.d.get("precipProbability")*100, 0), + "humidity": round(forecast.d.get("humidity")*100, 2), + "cloud_coverage": round(forecast.d.get("cloudCover")*100, 0), "native_wind_speed": round(forecast.d.get("windSpeed"), 2), "wind_bearing": round(forecast.d.get("windBearing"), 0), } - + def _map_hourly_forecast(forecast: dict[str, Any]) -> Forecast: return { - "datetime": utc_from_timestamp(forecast.d.get("time")).isoformat(), - "condition": MAP_CONDITION.get(forecast.d.get("icon")), + "datetime": utc_from_timestamp(forecast.d.get("time")).isoformat(), + "condition": MAP_CONDITION.get(forecast.d.get("icon")), "native_temperature": forecast.d.get("temperature"), "native_apparent_temperature": forecast.d.get("apparentTemperature"), "native_dew_point": forecast.d.get("dewPoint"), "native_pressure": forecast.d.get("pressure"), - "native_wind_speed": round(forecast.d.get("windSpeed"), 2), - "wind_bearing": round(forecast.d.get("windBearing"), 0), - "humidity": round(forecast.d.get("humidity") * 100, 2), + "native_wind_speed":round(forecast.d.get("windSpeed"), 2), + "wind_bearing": round(forecast.d.get("windBearing"), 0), + "humidity": round(forecast.d.get("humidity")*100, 2), "native_precipitation": round(forecast.d.get("precipIntensity"), 2), - "precipitation_probability": round( - forecast.d.get("precipProbability") * 100, 0 - ), - "cloud_coverage": round(forecast.d.get("cloudCover") * 100, 0), - "uv_index": round(forecast.d.get("uvIndex"), 2), - } - + "precipitation_probability":round(forecast.d.get("precipProbability")*100, 0), + "cloud_coverage": round(forecast.d.get("cloudCover")*100, 0), + "uv_index": round(forecast.d.get("uvIndex"), 2), + } async def async_setup_entry( hass: HomeAssistant, @@ -173,35 +203,33 @@ async def async_setup_entry( api_key = domain_data[CONF_API_KEY] latitude = domain_data[CONF_LATITUDE] longitude = domain_data[CONF_LONGITUDE] - # units = domain_data[CONF_UNITS] + #units = domain_data[CONF_UNITS] units = "si" forecast_mode = domain_data[CONF_MODE] - + unique_id = f"{config_entry.unique_id}" - + # Round Output - outputRound = domain_data[PW_ROUND] - - pw_weather = PirateWeather( - name, unique_id, forecast_mode, weather_coordinator, outputRound - ) + outputRound = domain_data[PW_ROUND] + + pw_weather = PirateWeather(name, unique_id, forecast_mode, weather_coordinator, outputRound) async_add_entities([pw_weather], False) - # _LOGGER.info(pw_weather.__dict__) - + #_LOGGER.info(pw_weather.__dict__) + class PirateWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]): """Implementation of an PirateWeather sensor.""" _attr_attribution = ATTRIBUTION _attr_should_poll = False - + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_pressure_unit = UnitOfPressure.MBAR _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_visibility_unit = UnitOfLength.KILOMETERS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND - + def __init__( self, name: str, @@ -213,28 +241,28 @@ def __init__( """Initialize the sensor.""" super().__init__(weather_coordinator) self._attr_name = name - # self._attr_device_info = DeviceInfo( + #self._attr_device_info = DeviceInfo( # entry_type=DeviceEntryType.SERVICE, # identifiers={(DOMAIN, unique_id)}, # manufacturer=MANUFACTURER, # name=DEFAULT_NAME, - # ) + #) self._weather_coordinator = weather_coordinator self._name = name self._mode = forecast_mode - self._unique_id = unique_id + self._unique_id = unique_id self._ds_data = self._weather_coordinator.data self._ds_currently = self._weather_coordinator.data.currently() self._ds_hourly = self._weather_coordinator.data.hourly() self._ds_daily = self._weather_coordinator.data.daily() - + self.outputRound = outputRound @property def unique_id(self): """Return a unique_id for this entity.""" return self._unique_id - + @property def supported_features(self) -> WeatherEntityFeature: """Determine supported features based on available data sets reported by WeatherKit.""" @@ -242,8 +270,8 @@ def supported_features(self) -> WeatherEntityFeature: features |= WeatherEntityFeature.FORECAST_DAILY features |= WeatherEntityFeature.FORECAST_HOURLY - return features - + return features + @property def available(self): """Return if weather data is available from PirateWeather.""" @@ -263,32 +291,33 @@ def name(self): def native_temperature(self): """Return the temperature.""" temperature = self._weather_coordinator.data.currently().d.get("temperature") - - if self.outputRound == "Yes": - return round(temperature, 0) + 0 + + if self.outputRound=="Yes": + return round(temperature, 0)+0 else: - return round(temperature, 2) - + return round(temperature, 2) + @property def humidity(self): """Return the humidity.""" humidity = self._weather_coordinator.data.currently().d.get("humidity") * 100.0 - if self.outputRound == "Yes": - return round(humidity, 0) + 0 + if self.outputRound=="Yes": + return round(humidity, 0)+0 else: - return round(humidity, 2) - + return round(humidity, 2) + + @property def native_wind_speed(self): """Return the wind speed.""" windspeed = self._weather_coordinator.data.currently().d.get("windSpeed") - if self.outputRound == "Yes": - return round(windspeed, 0) + 0 + if self.outputRound=="Yes": + return round(windspeed, 0)+0 else: - return round(windspeed, 2) - + return round(windspeed, 2) + @property def wind_bearing(self): """Return the wind bearing.""" @@ -298,43 +327,43 @@ def wind_bearing(self): def ozone(self): """Return the ozone level.""" ozone = self._weather_coordinator.data.currently().d.get("ozone") - - if self.outputRound == "Yes": - return round(ozone, 0) + 0 + + if self.outputRound=="Yes": + return round(ozone, 0)+0 else: - return round(ozone, 2) - + return round(ozone, 2) + @property def native_pressure(self): """Return the pressure.""" pressure = self._weather_coordinator.data.currently().d.get("pressure") - - if self.outputRound == "Yes": - return round(pressure, 0) + 0 + + if self.outputRound=="Yes": + return round(pressure, 0)+0 else: - return round(pressure, 2) + return round(pressure, 2) + @property def native_visibility(self): """Return the visibility.""" visibility = self._weather_coordinator.data.currently().d.get("visibility") - - if self.outputRound == "Yes": - return round(visibility, 0) + 0 + + if self.outputRound=="Yes": + return round(visibility, 0)+0 else: - return round(visibility, 2) + return round(visibility, 2) @property def condition(self): """Return the weather condition.""" - return MAP_CONDITION.get( - self._weather_coordinator.data.currently().d.get("icon") - ) + return MAP_CONDITION.get(self._weather_coordinator.data.currently().d.get("icon")) + @callback def _async_forecast_daily(self) -> list[Forecast] | None: - """Return the daily forecast.""" + """Return the daily forecast.""" daily_forecast = self._weather_coordinator.data.daily().data if not daily_forecast: return None @@ -343,28 +372,29 @@ def _async_forecast_daily(self) -> list[Forecast] | None: @callback def _async_forecast_hourly(self) -> list[Forecast] | None: - """Return the hourly forecast.""" + """Return the hourly forecast.""" hourly_forecast = self._weather_coordinator.data.hourly().data - + if not hourly_forecast: return None return [_map_hourly_forecast(f) for f in hourly_forecast] - + + async def async_update(self) -> None: """Get the latest data from PW and updates the states.""" - await self._weather_coordinator.async_request_refresh() - - # async def update(self): - # """Get the latest data from Dark Sky.""" - # await self._dark_sky.update() - # - # self._ds_data = self._dark_sky.data - # currently = self._dark_sky.currently - # self._ds_currently = currently.d if currently else {} - # self._ds_hourly = self._dark_sky.hourly - # self._ds_daily = self._dark_sky.daily - + await self._weather_coordinator.async_request_refresh() + +# async def update(self): +# """Get the latest data from Dark Sky.""" +# await self._dark_sky.update() +# +# self._ds_data = self._dark_sky.data +# currently = self._dark_sky.currently +# self._ds_currently = currently.d if currently else {} +# self._ds_hourly = self._dark_sky.hourly +# self._ds_daily = self._dark_sky.daily + async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" self.async_on_remove( diff --git a/custom_components/pirateweather/weather_update_coordinator.py b/custom_components/pirateweather/weather_update_coordinator.py index 3764bbb..a354fed 100644 --- a/custom_components/pirateweather/weather_update_coordinator.py +++ b/custom_components/pirateweather/weather_update_coordinator.py @@ -1,13 +1,20 @@ """Weather data coordinator for the OpenWeatherMap (OWM) service.""" +from datetime import timedelta import logging import async_timeout +import forecastio from forecastio.models import Forecast import json import aiohttp +import asyncio +from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout +import voluptuous as vol +from homeassistant.helpers import sun, aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt from .const import ( @@ -18,7 +25,7 @@ ATTRIBUTION = "Powered by Pirate Weather" - + class WeatherUpdateCoordinator(DataUpdateCoordinator): """Weather data update coordinator.""" @@ -29,52 +36,45 @@ def __init__(self, api_key, latitude, longitude, pw_scan_Int, hass): self.longitude = longitude self.pw_scan_Int = pw_scan_Int self.requested_units = "si" - + self.data = None self.currently = None self.hourly = None self.daily = None self._connect_error = False - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=pw_scan_Int) - + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=pw_scan_Int + ) + + + async def _async_update_data(self): """Update the data.""" data = {} async with async_timeout.timeout(60): try: data = await self._get_pw_weather() - _LOGGER.info( - "Pirate Weather data update for " - + str(self.latitude) - + "," - + str(self.longitude) - ) + _LOGGER.info('Pirate Weather data update for ' + str(self.latitude) + ',' + str(self.longitude)) except Exception as err: - raise UpdateFailed(f"Error communicating with API: {err}") + raise UpdateFailed(f"Error communicating with API: {err}") return data - async def _get_pw_weather(self): - """Poll weather data from PW.""" - forecastString = ( - "https://api.pirateweather.net/forecast/" - + self._api_key - + "/" - + str(self.latitude) - + "," - + str(self.longitude) - + "?units=" - + self.requested_units - + "&extend=hourly" - ) + async def _get_pw_weather(self): + """Poll weather data from PW.""" + + forecastString = "https://api.pirateweather.net/forecast/" + self._api_key + "/" + str(self.latitude) + "," + str(self.longitude) + "?units=" + self.requested_units + "&extend=hourly" + async with aiohttp.ClientSession(raise_for_status=True) as session: - async with session.get(forecastString) as resp: - resptext = await resp.text() - jsonText = json.loads(resptext) - headers = resp.headers - status = resp.raise_for_status() - - data = Forecast(jsonText, status, headers) + async with session.get(forecastString) as resp: + + resptext = await resp.text() + jsonText = json.loads(resptext) + headers = resp.headers + status = resp.raise_for_status() + + data = Forecast(jsonText, status, headers) return data + diff --git a/example.png b/example.png new file mode 100644 index 0000000000000000000000000000000000000000..074f0ab3fcf0150562b001689e041af55c630898 GIT binary patch literal 27860 zcmeFZd010d`vn@b)+#EsC{7Tpwqik1vC5 z^6k~b2VJbPwNHIux}^J{VjB%{x(mOiZyrL?k0nd$J`4sMY|GPmG8?uzeiQ~fu+|y|d-0wQ z?9r$HkzXAlsm2}E;{qxNMaT(0r42@I%Z{ERs>e)v{cv78!(fl>riVvV!?YZh6|G{a6?kInb3Gj$$LCzdZSL!LFOxlq(o-LwNDAA9yIpU_B&s9Hya$6?X)zaf zPNi7?AX>g3X>Gk>iK^FjDu$y*due^}brqiQVR1>A*yVQB9cF^sr)-V++?UoOWhYlj zh#t8|VeS@;sHYi>SMoa7;qW%~Be(NyJmm-a>(VQ-&7 zZ-0W`wO|pUW-<3k`6jIozz31B%A;l02}h$T*dIdUtPMFur8a^&-?w`?D2xh_Sg&?@ z?Nmt;Sooc8R+hoRq1;&?rSJThQWNEBN+h*$g?V+%%gjcMr?;VWR+#I^rLyTQM@od? zDOq+!YF`WM+hXoJbW)m}jN%BNVMrlMIQU4|gQ>Hwri3KF5057KhV&!FLwPk-q-FZU zHgmqP4rxAabC{mXqT;=#GdYQTFP`PHc(&Gqb~(*?>f7%P(9cGaVq;>|;orKQE?vG_ zE@dTp_~#nksONn_mQLQ5Kd2ihbiy@pkV4ob9_!oQdj=ON7x!$#99u_yAnH!}xD zwO2km;kmy0$NR{&>z6#4Z7+!hzZmm7RQiSD?z^Xv5eqh&9^O7^wyG()YtdN=!~_D;jz&z$bU#uvkvd_4bu|HH*eTa_rf(j_Uq zEsI^zV%F#Fo!2lRM!Vs2MzqZ-bXms`X{6mdeMFkm=G7J*^?i#k~#L%E%J*irHv)hc&nB1K3JOq@HTlBMs;X#e;0Ejcm+}DQ_}W` z4OCQeY!b(dnTguil8r4@q&~=rlcYJ}GN0#xos}S2S^FMwmuqE+JL41DWU$Q*B&DGy z$RKv*vY-|VZ)vyqdS##S&8gjQ`uTyrN#VNI&fk^?gvbjQ#(=<>7-$ZqI zy(>QQ#Yxxin3=7sq%!~T9%f-t&&tWSgIm!OmhvTS{Jv6ij&bOLT%YXMk-HP{+%#8) zvxKa&UdGJjO4&c!g)PYy$3L8mFALq^T4t2sg+r0ip>-nyVb>7pN`IE-D*2uyZGdn_d>aO_7+Pq39_uBBLY^y|NtE#z;I@oLe+<$%`r7iYdMYBbMxT5$F5`m1*X1aoO0&VsdFXQ+e>n_MWy ze7N+zt$RRlS(LN$)+Zp6o&93YlLKHY-QR8ee!$gH*m;A)7dAVCZ6&%k{~ZcfJn{&a)BL*5@B6R50@1SKqnOW3K7yj9!C(Hq zAEVVEdc0g?4Lg$Mvfc$6@Alz4hc-f!+;?#o6`I5+OXs)#;|I_@6k&+kX`dA$;AxdI zG#6WHDKDi7T>^;alOwkf@bi_;oUR)=hqCp0LuLuSvTEJ)DNa9hETI;*$X$`rxkWRs z!}p9r$<8xha)G_~szh;Y zu<%4TKu$=wr{wo`blCY+F_v_tch6#)YX5>JtaoV{6(aTf23KGouEb!p6s%D%hn4)Po3 zyax_$X(P9TEd)-&CN@&(#XK%$Mr$3HnLnRgDTp)}D06ZgSYFF-LB`I%7_=B}bjYe~ zTLLaTSls4<@A^>bMQE`;f0J4n<*h>2p%a=-#NWN5fP%8I{@#TuTBFmgIKr_aAM0AI^~WaTsOV2^R3Ev(ja@>eg

;}a*$o73ZP<^Q(1XNx=E~7! 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

Up6`Ch1Z@ERy10Ab~V5mWD8_c7HqGK70~ zuulOq3XHd5%OAl1Hxlp~jYeUo!|?UniZ46Pw6%z9-BIE1=Ih`2Lm;sTVtE3;C$3BE zdUwDs7+Fzh{|)Owv(rI%hW$y)xQ3!yEbEl~hm)U;?!t}Uj%@e>*_+yVS^t@1ITYs=Pkwwh+y_?tJnTT76_fhsQe%dcI2#&??Oz z;-!DrWbVgUL$vyhMF#F{itW+CgSPy7H0$ZvJ!?`!5G0lJ#0M+$#E%LyMpJuYho z@a7~Zl(}B4;k_y{$r9;U*?f$UmmZ1BkpBr7rQ91ELDeIQi_(6EP8R7yCADV?>z_~D z#f^=@Edg1R)-M%om!RxhR$W_fwY*T_s=6!RjvhLNOzH6E* zBDD_zLmXcMgmgOshcFX_r7j;RgL9s9PM9~@_nRm z>4}mLGOhr!@ylJ7|D1rH#V?iH{%_$qNZBeVn~h7`nQ+CRd=bZY5-=$!_6nvn@0p)iKD8N#KE7i@3BZ>kY?k5xJKBQlfH zM~7g>Z6~etI9&Yzrn;G5!E%$V&avRG6ejnOvxNwH=Jbw{(~jVg2wv+XrloqBb5>lqMdBo`c;-l1vZg!it>x|`aaon;vcH?|2_Z! literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e0591db..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -colorlog==6.8.0 -homeassistant==2023.10.0 -pip>=21.0,<23.4 -ruff==0.1.9 \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..7d78f01 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1 @@ +homeassistant diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..71f5999 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1 @@ +pytest-homeassistant-custom-component diff --git a/scripts/develop b/scripts/develop deleted file mode 100644 index 89eda50..0000000 --- a/scripts/develop +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -# Create config dir if not present -if [[ ! -d "${PWD}/config" ]]; then - mkdir -p "${PWD}/config" - hass --config "${PWD}/config" --script ensure_config -fi - -# Set the path to custom_components -## This let's us have the structure we want /custom_components/integration_blueprint -## while at the same time have Home Assistant configuration inside /config -## without resulting to symlinks. -export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" - -# Start Home Assistant -hass --config "${PWD}/config" --debug diff --git a/scripts/lint b/scripts/lint deleted file mode 100644 index 9b5b1df..0000000 --- a/scripts/lint +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -ruff check . --fix diff --git a/scripts/setup b/scripts/setup deleted file mode 100644 index 141d19f..0000000 --- a/scripts/setup +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -python3 -m pip install --requirement requirements.txt