diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index aa335e4..13fcb5f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,38 +1,38 @@ -# Use an official Python image as the base image -FROM python:3.12-slim - -# Set environment variables -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - git \ - curl \ - libffi-dev \ - libssl-dev \ - libjpeg-dev \ - zlib1g-dev \ - gcc \ - make \ - && rm -rf /var/lib/apt/lists/* - -# Set the timezone to Europe/Vilnius -ENV TZ=Europe/Vilnius - -# Set up the user for vscode -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -RUN groupadd --gid $USER_GID $USERNAME \ - && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME - -# Switch to the vscode user -USER $USERNAME - -# Set the working directory -WORKDIR /home/vscode - -# Expose the default Home Assistant port -EXPOSE 8123 +# Use an official Python image as the base image +FROM python:3.12-slim + +# Set environment variables +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + libffi-dev \ + libssl-dev \ + libjpeg-dev \ + zlib1g-dev \ + gcc \ + make \ + && rm -rf /var/lib/apt/lists/* + +# Set the timezone to Europe/Vilnius +ENV TZ=Europe/Vilnius + +# Set up the user for vscode +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME + +# Switch to the vscode user +USER $USERNAME + +# Set the working directory +WORKDIR /home/vscode + +# Expose the default Home Assistant port +EXPOSE 8123 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8e98575..34941ad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,37 +1,45 @@ -{ - "name": "Home Assistant Dev Container", - "build": { - "dockerfile": "Dockerfile", - "context": ".." - }, - "customizations": { - "vscode": { - "settings": { - "python.pythonPath": "/usr/local/bin/python", - "terminal.integrated.shell.linux": "/bin/bash" - }, - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "ms-toolsai.jupyter", - "ms-python.black-formatter", - "ms-python.autopep8", - "ms-python.pylint", - "ms-vscode.remote-containers", - "charliermarsh.ruff" - ] - } - }, - "postCreateCommand": "pip install -r requirements.txt", - "remoteUser": "vscode", - "mounts": [ - "source=${localWorkspaceFolder}/config,target=/config,type=bind", - "source=${localWorkspaceFolder}/custom_components,target=/config/custom_components,type=bind" - ], - "forwardPorts": [ - 8123 - ], - "remoteEnv": { - "TZ": "Europe/Vilnius" - } +{ + "name": "Home Assistant Dev Container", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "customizations": { + "vscode": { + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.selectInterpreter": "/usr/local/bin/python", + "terminal.integrated.shell.linux": "/bin/bash" + }, + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-toolsai.jupyter", + "ms-python.black-formatter", + "ms-python.autopep8", + "ms-python.pylint", + "ms-vscode.remote-containers", + "charliermarsh.ruff", + "yzhang.markdown-all-in-one", + "kevinrose.vsc-python-indent", + "keesschollaart.vscode-home-assistant", + "donjayamanne.githistory", + "mhutchie.git-graph", + "mikoz.black-py" + ] + } + }, + "postCreateCommand": "pip install -r requirements.txt", + "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", + "remoteUser": "vscode", + "mounts": [ + "source=${localWorkspaceFolder}/config,target=/config,type=bind", + "source=${localWorkspaceFolder}/custom_components,target=/config/custom_components,type=bind" + ], + "forwardPorts": [ + 8123 + ], + "remoteEnv": { + "TZ": "Europe/Vilnius" + } } \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2657933 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + # Maintain dependencies for pip + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..47e3e9a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: "Lint" + +on: + push: + paths-ignore: + - "**/README.md" + - "**/CHANGELOG.md" + - "**/CONTRIBUTING.md" + - "**/requirements.txt" + branches: + - "main" + pull_request: + branches: + - "main" + +jobs: + ruff: + name: "Ruff" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout the repository" + uses: "actions/checkout@v4.1.7" + + - name: "Set up Python" + uses: actions/setup-python@v5.1.0 + with: + python-version: "3.12" + cache: "pip" + + - name: "Install requirements" + run: python3 -m pip install -r requirements.txt + + - name: "Run" + run: python3 -m ruff check . \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ec0e627 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: "Release" + +on: + release: + types: + - "published" + +permissions: {} + +jobs: + release: + name: "Release" + runs-on: "ubuntu-latest" + permissions: + contents: write + steps: + - name: "Checkout the repository" + uses: "actions/checkout@v4.1.7" + + - name: "Adjust version number" + shell: "bash" + run: | + yq -i -o json '.version="${{ github.event.release.tag_name }}"' \ + "${{ github.workspace }}/custom_components/meteo_lt/manifest.json" + + - name: "ZIP the integration directory" + shell: "bash" + run: | + cd "${{ github.workspace }}/custom_components/meteo_lt" + zip meteo_lt.zip -r ./ + + - name: "Upload the ZIP file to the release" + uses: softprops/action-gh-release@v2.0.6 + with: + files: ${{ github.workspace }}/custom_components/meteo_lt/meteo_lt.zip \ No newline at end of file diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 0000000..d2256db --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,40 @@ +name: "Validate" + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + push: + paths-ignore: + - "**/README.md" + - "**/CHANGELOG.md" + - "**/CONTRIBUTING.md" + - "**/requirements.txt" + branches: + - "main" + pull_request: + branches: + - "main" + +jobs: + hassfest: # https://developers.home-assistant.io/blog/2020/04/16/hassfest + name: "Hassfest Validation" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout the repository" + uses: "actions/checkout@v4.1.7" + + - name: "Run hassfest validation" + uses: "home-assistant/actions/hassfest@master" + + hacs: # https://github.com/hacs/action + name: "HACS Validation" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout the repository" + uses: "actions/checkout@v4.1.7" + + - name: "Run HACS validation" + uses: "hacs/action@main" + with: + category: "integration" \ No newline at end of file diff --git a/.gitignore b/.gitignore index ab74e15..0141d99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,163 +1,163 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ -.DS_Store +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json index a04b218..2e9b3a6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ -{ - "files.associations": { - "*.yaml": "home-assistant" - } +{ + "files.associations": { + "*.yaml": "home-assistant" + } } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..94ccd1d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +## Not a Release 0.1.x + +Date: `2024-07-24` + +### Changes + +- Initial version moved from local HASS \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2060e63 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# 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 MIT Software License + +In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. + +## Report bugs using Github's [issues](../../issues) + +GitHub issues are used to track public bugs. +Report a bug by [opening a new issue](../../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 MIT License. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 83a51f2..c37af4f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2024 Ramūnas Bunokas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2024 Ramūnas Bunokas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 1fca74f..a63b92a 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,70 @@ -# Meteo.LT integration for Home Assistant -Home Assistant integration for Meteo.Lt REST API - -[![GitHub Release][releases-shield]][releases] -[![GitHub Activity][commits-shield]][commits] -[![License][license-shield]](LICENSE) -![Project Maintenance][maintenance-shield] - -Buy Me A Coffee - -This integration adds support for retrieving the Forecast data from [ Api.Meteo.Lt](https://api.meteo.lt). - -This integration will set up the Home Assistant `weather` entity, with current data, and hourly forecast data. The first Forecast data record is treated as current data. - -Minimum required version of Home Assistant is **2024.7.3** as this integration uses the new Weather entity forecast types and it does **not** create Forecast Attributes. - -## Installation through HACS (Recommended Method) - -This Integration is part of the default HACS store. Search for *Meteo.lt* under Integrations and install from there. After the installation of the files, you must restart Home Assistant, or else you will not be able to add WeatherFlow Forecast from the Integration Page. - -If you are not familiar with HACS, or haven't installed it, I would recommend to [look through the HACS documentation](https://hacs.xyz/), before continuing. Even though you can install the Integration manually, I would recommend using HACS, as you would always be reminded when a new release is published. - -## Manual Installation - -1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). -1. If you do not have a `custom_components` directory (folder) there, you need to create it. -1. In the `custom_components` directory (folder) create a new folder called `meteo_lt`. -1. Download _all_ the files from the `custom_components/meteo_lt/` directory (folder) in this repository. -1. Place the files you downloaded in the new directory (folder) you created. -1. Restart Home Assistant -1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Meteo.Lt" - -## Enable Debug Logging - -If logs are needed for debugging or reporting an issue, use the following configuration.yaml: - -```yaml -logger: - default: error - logs: - custom_components.meteo_lt: debug -``` - -*** - -[commits-shield]: https://img.shields.io/github/commit-activity/y/Brunas/meteo_lt.svg?style=flat-square -[commits]: https://github.com/Brunas/meteo_lt/commits/main -[hacs]: https://github.com/hacs/integration -[hacsbadge]: https://img.shields.io/badge/HACS-Default-orange.svg?style=flat-square -[license-shield]: https://img.shields.io/github/license/Brunas/meteo_lt.svg?style=flat-square -[maintenance-shield]: https://img.shields.io/badge/maintainer-Brunas%20%40Brunas-blue.svg?style=flat-square -[releases-shield]: https://img.shields.io/github/release/Brunas/meteo_lt.svg?style=flat-square -[releases]: https://github.com/Brunas/meteo_lt/releases \ No newline at end of file +# Meteo.LT integration for Home Assistant + + +Home Assistant integration for Meteo.Lt REST API + +[![GitHub Release][releases-shield]][releases] +[![GitHub Activity][commits-shield]][commits] +[![License][license-shield]](LICENSE) +![Project Maintenance][maintenance-shield] +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +Buy Me A Coffee + +This integration adds support for retrieving the Forecast data from [Api.Meteo.Lt](https://api.meteo.lt) and setting up following platforms in Home Assistant: + +Platform | Description +-- | -- +`weather` | A Home Assistant `weather` entity, with current data, and hourly forecast data. The first forecast record is treated as current data. +`sensor` | A Home Assistant `sensor` entity, with all available data taken from the forecast first record. + +Minimum required version of Home Assistant is **2024.7.3** as this integration uses the new Weather entity forecast types and it does **not** create Forecast Attributes. + +## Installation through HACS (Recommended Method) + +This Integration is part of the default HACS store. Search for *Meteo.lt* under Integrations and install from there. After the installation of the files, you must restart Home Assistant, or else you will not be able to add WeatherFlow Forecast from the Integration Page. + +If you are not familiar with HACS, or haven't installed it, I would recommend to [look through the HACS documentation](https://hacs.xyz/), before continuing. Even though you can install the Integration manually, I would recommend using HACS, as you would always be reminded when a new release is published. + +## Manual Installation + +1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). +1. If you do not have a `custom_components` directory (folder) there, you need to create it. +1. In the `custom_components` directory (folder) create a new folder called `meteo_lt`. +1. Download _all_ the files from the `custom_components/meteo_lt/` directory (folder) in this repository. +1. Place the files you downloaded in the new directory (folder) you created. +1. Restart Home Assistant +1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Meteo.Lt": + - Enter latitude and longitude to use for the integration. Default values are Home Assistant Home location. + - Unlimitted number of locations is supported - `weather.meteo_lt_ABCD` and `sensor.meteo_lt_ABCD_current_conditions` entities are created where `ABCD` is name of the nearest calculated place from available places in api.meteo.lt. Note, that if an entity for the same place exists, new entity gets numeric suffix to the name. + +## Enable Debug Logging + +If logs are needed for debugging or reporting an issue, turn debugging in integration UI or use the following configuration.yaml: + +```yaml +logger: + default: error + logs: + custom_components.meteo_lt: debug +``` + +## Inspired by + +[WeatherFlow Cloud](https://www.home-assistant.io/integrations/weatherflow_cloud/) + +[SMHI](https://www.home-assistant.io/integrations/smhi/) + +[OpenWeatherMap](https://www.home-assistant.io/integrations/openweathermap/) + + +*** + +[commits-shield]: https://img.shields.io/github/commit-activity/y/Brunas/meteo_lt.svg?style=flat-square +[commits]: https://github.com/Brunas/meteo_lt/commits/main +[hacs]: https://github.com/hacs/integration +[hacsbadge]: https://img.shields.io/badge/HACS-Default-orange.svg?style=flat-square +[license-shield]: https://img.shields.io/github/license/Brunas/meteo_lt.svg?style=flat-square +[maintenance-shield]: https://img.shields.io/badge/maintainer-Brunas%20%40Brunas-blue.svg?style=flat-square +[releases-shield]: https://img.shields.io/github/release/Brunas/meteo_lt.svg?style=flat-square +[releases]: https://github.com/Brunas/meteo_lt/releases diff --git a/custom_components/meteo_lt/__init__.py b/custom_components/meteo_lt/__init__.py index 39d5197..c63db13 100644 --- a/custom_components/meteo_lt/__init__.py +++ b/custom_components/meteo_lt/__init__.py @@ -1,50 +1,57 @@ -"""__init__.py""" - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -from meteo_lt import MeteoLtAPI -from .const import DOMAIN, LOGGER -from .coordinator import MeteoLtCoordinator - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Meteo.Lt component.""" - LOGGER.info("Setting up Meteo.Lt") - hass.data.setdefault(DOMAIN, {}) - return True - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Meteo.Lt from a config entry.""" - LOGGER.info("Setting up Meteo.Lt from config entry") - - api = MeteoLtAPI() - latitude = entry.data.get("latitude", hass.config.latitude) - longitude = entry.data.get("longitude", hass.config.longitude) - LOGGER.debug("Configured coordinates: %s, %s", latitude, longitude) - - nearest_place = await api.get_nearest_place(latitude, longitude) - LOGGER.debug("Nearest place found: %s", nearest_place) - - coordinator = MeteoLtCoordinator(hass, api, nearest_place) - await coordinator.async_refresh() - - hass.data[DOMAIN][entry.entry_id] = { - "api": api, - "nearest_place": nearest_place, - "coordinator": coordinator, - } - - await hass.config_entries.async_forward_entry_setups(entry, ["weather"]) - - return True - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - LOGGER.info("Unloading Meteo.Lt config entry") - - unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "weather") - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok +"""__init__.py""" + +from typing import Final + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from meteo_lt import MeteoLtAPI +from .const import DOMAIN, MANUFACTURER, LOGGER +from .coordinator import MeteoLtCoordinator + +PLATFORMS: Final = [Platform.WEATHER, Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Meteo.Lt from a config entry.""" + LOGGER.info("Setting up %s from config entry", MANUFACTURER) + + hass.data.setdefault(DOMAIN, {}) + + api = MeteoLtAPI() + latitude = entry.data.get("latitude", hass.config.latitude) + longitude = entry.data.get("longitude", hass.config.longitude) + LOGGER.debug("Configured coordinates: %s, %s", latitude, longitude) + + nearest_place = await api.get_nearest_place(latitude, longitude) + LOGGER.debug("Nearest place found: %s", nearest_place) + + coordinator = MeteoLtCoordinator(hass, api, nearest_place) + await coordinator.async_refresh() + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + hass.data[DOMAIN][entry.entry_id] = { + "api": api, + "nearest_place": nearest_place, + "coordinator": coordinator, + } + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +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.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/custom_components/meteo_lt/config_flow.py b/custom_components/meteo_lt/config_flow.py index 7651c81..bbecd35 100644 --- a/custom_components/meteo_lt/config_flow.py +++ b/custom_components/meteo_lt/config_flow.py @@ -1,94 +1,89 @@ -"""config_flow.py""" - -# pylint: disable=too-few-public-methods, broad-exception-caught - -import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.typing import ConfigType -from homeassistant.data_entry_flow import FlowResult - -from .const import DOMAIN - - -@config_entries.HANDLERS.register(DOMAIN) -class MeteoLtConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Meteo.Lt.""" - - VERSION = 1 - - @callback - def _show_config_form(self, step_id, user_input=None, errors=None): - """Show the configuration form.""" - step_id = step_id or "user" - user_input = user_input or {} - data_schema = vol.Schema( - { - vol.Required( - "latitude", - default=user_input.get("latitude", self.hass.config.latitude), - ): vol.Coerce(float), - vol.Required( - "longitude", - default=user_input.get("longitude", self.hass.config.longitude), - ): vol.Coerce(float), - } - ) - return self.async_show_form( - step_id=step_id, - data_schema=data_schema, - errors=errors, - description_placeholders={"name": "Meteo.Lt"}, - ) - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - errors = {} - if user_input is not None: - await self.async_set_unique_id( - f"{DOMAIN}-{user_input['latitude']}-{user_input['longitude']}" - ) - self._abort_if_unique_id_configured() - return self.async_create_entry(title="Meteo.Lt", data=user_input) - return self._show_config_form("user", user_input, errors) - - async def async_step_reconfigure(self, user_input=None) -> FlowResult: - """Handle the reconfiguration step.""" - errors = {} - - if user_input is not None: - entry_id = self.context["entry_id"] - entry = self.hass.config_entries.async_get_entry(entry_id) - if entry: - new_data = dict(entry.data) - new_data.update(user_input) - self.hass.config_entries.async_update_entry(entry, data=new_data) - return self.async_create_entry(title="Meteo.Lt", data=new_data) - else: - errors["base"] = "cannot_connect" - - entry_id = self.context["entry_id"] - entry = self.hass.config_entries.async_get_entry(entry_id) - if entry: - current_config = entry.data - default_latitude = current_config.get("latitude", self.hass.config.latitude) - default_longitude = current_config.get( - "longitude", self.hass.config.longitude - ) - else: - default_latitude = self.hass.config.latitude - default_longitude = self.hass.config.longitude - - return self._show_config_form( - "reconfigure", - {"latitude": default_latitude, "longitude": default_longitude}, - errors, - ) - - async def async_step_reconfigure_confirm(self, user_input=None) -> FlowResult: - """Handle confirmation of reconfiguration.""" - if user_input is not None: - return await self.async_step_reconfigure() - - return self._show_config_form("reconfigure_confirm") +"""config_flow.py""" + +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN, MANUFACTURER + + +@config_entries.HANDLERS.register(DOMAIN) +class MeteoLtConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Meteo.Lt.""" + + VERSION = 1 + + @callback + def _show_config_form(self, step_id, user_input=None, errors=None): + """Show the configuration form.""" + step_id = step_id or "user" + user_input = user_input or {} + data_schema = vol.Schema( + { + vol.Required( + "latitude", + default=user_input.get("latitude", self.hass.config.latitude), + ): vol.Coerce(float), + vol.Required( + "longitude", + default=user_input.get("longitude", self.hass.config.longitude), + ): vol.Coerce(float), + } + ) + return self.async_show_form( + step_id=step_id, + data_schema=data_schema, + errors=errors, + description_placeholders={"name": MANUFACTURER}, + ) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + await self.async_set_unique_id( + f"{DOMAIN}-{user_input['latitude']}-{user_input['longitude']}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=MANUFACTURER, data=user_input) + return self._show_config_form("user", user_input, errors) + + async def async_step_reconfigure(self, user_input=None) -> FlowResult: + """Handle the reconfiguration step.""" + errors = {} + + if user_input is not None: + entry_id = self.context["entry_id"] + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry: + new_data = dict(entry.data) + new_data.update(user_input) + self.hass.config_entries.async_update_entry(entry, data=new_data) + return self.async_create_entry(title=MANUFACTURER, data=new_data) + errors["base"] = "cannot_connect" + + entry_id = self.context["entry_id"] + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry: + current_config = entry.data + default_latitude = current_config.get("latitude", self.hass.config.latitude) + default_longitude = current_config.get( + "longitude", self.hass.config.longitude + ) + else: + default_latitude = self.hass.config.latitude + default_longitude = self.hass.config.longitude + + return self._show_config_form( + "reconfigure", + {"latitude": default_latitude, "longitude": default_longitude}, + errors, + ) + + async def async_step_reconfigure_confirm(self, user_input=None) -> FlowResult: + """Handle confirmation of reconfiguration.""" + if user_input is not None: + return await self.async_step_reconfigure() + + return self._show_config_form("reconfigure_confirm") diff --git a/custom_components/meteo_lt/const.py b/custom_components/meteo_lt/const.py index 4f9aa5a..5b539f0 100644 --- a/custom_components/meteo_lt/const.py +++ b/custom_components/meteo_lt/const.py @@ -1,7 +1,8 @@ -"""const.py""" - -import logging - -DOMAIN = "meteo_lt" -UPDATE_MINUTES = 30 -LOGGER = logging.getLogger(__package__) +"""const.py""" + +import logging + +DOMAIN = "meteo_lt" +MANUFACTURER = "Meteo.Lt" +UPDATE_MINUTES = 30 +LOGGER = logging.getLogger(__package__) diff --git a/custom_components/meteo_lt/coordinator.py b/custom_components/meteo_lt/coordinator.py index e210dec..65587db 100644 --- a/custom_components/meteo_lt/coordinator.py +++ b/custom_components/meteo_lt/coordinator.py @@ -1,27 +1,27 @@ -"""coordinator.py""" - -from datetime import timedelta -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import LOGGER, UPDATE_MINUTES - - -class MeteoLtCoordinator(DataUpdateCoordinator): - """Class to manage fetching Meteo LT data.""" - - def __init__(self, hass, api, nearest_place): - """Initialize.""" - self.api = api - self.nearest_place = nearest_place - super().__init__( - hass, - LOGGER, - name="Meteo LT", - update_interval=timedelta(minutes=UPDATE_MINUTES), - ) - - async def _async_update_data(self): - """Fetch data from API.""" - forecast = await self.api.get_forecast(self.nearest_place.code) - LOGGER.debug("Forecast retrieved: %s", forecast) - return forecast +"""coordinator.py""" + +from datetime import timedelta +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import MANUFACTURER, LOGGER, UPDATE_MINUTES + + +class MeteoLtCoordinator(DataUpdateCoordinator): + """Class to manage fetching Meteo LT data.""" + + def __init__(self, hass, api, nearest_place): + """Initialize.""" + self.api = api + self.nearest_place = nearest_place + super().__init__( + hass, + LOGGER, + name=MANUFACTURER, + update_interval=timedelta(minutes=UPDATE_MINUTES), + ) + + async def _async_update_data(self): + """Fetch data from API.""" + forecast = await self.api.get_forecast(self.nearest_place.code) + LOGGER.debug("Forecast retrieved: %s", forecast) + return forecast diff --git a/custom_components/meteo_lt/manifest.json b/custom_components/meteo_lt/manifest.json index eb4dd17..1348990 100644 --- a/custom_components/meteo_lt/manifest.json +++ b/custom_components/meteo_lt/manifest.json @@ -1,14 +1,14 @@ -{ - "domain": "meteo_lt", - "name": "Meteo.LT", - "version": "0.1.4", - "codeowners": ["@Brunas"], - "config_flow": true, - "documentation": "https://github.com/Brunas/meteo_lt", - "iot_class": "cloud_polling", - "integration_type": "service", - "loggers": ["meteo_lt"], - "requirements": ["meteo_lt-pkg==0.1.4"], - "dependencies": [], - "supported_platforms": ["weather"] +{ + "domain": "meteo_lt", + "name": "Meteo.Lt", + "codeowners": ["@Brunas"], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/Brunas/meteo_lt", + "integration_type": "service", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/Brunas/meteo_lt/issues", + "loggers": ["meteo_lt"], + "requirements": ["meteo_lt-pkg==0.1.4"], + "version": "0.1.5" } \ No newline at end of file diff --git a/custom_components/meteo_lt/sensor.py b/custom_components/meteo_lt/sensor.py new file mode 100644 index 0000000..937bf77 --- /dev/null +++ b/custom_components/meteo_lt/sensor.py @@ -0,0 +1,72 @@ +"""sensor.py""" + +from functools import cached_property +from typing import Any +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + UnitOfSpeed, + UnitOfTemperature, + UnitOfPressure, + UnitOfPrecipitationDepth, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN, LOGGER + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Meteo.Lt sensor based on a config entry.""" + LOGGER.debug( + "Sensor setting up input: hass.data - %s, config entry - %s", + hass.data[DOMAIN][entry.entry_id], + entry, + ) + async_add_entities( + [ + MeteoLtCurrentConditionsSensor( + hass.data[DOMAIN][entry.entry_id]["coordinator"], + hass.data[DOMAIN][entry.entry_id]["nearest_place"], + entry, + ) + ] + ) + + +class MeteoLtCurrentConditionsSensor(CoordinatorEntity, SensorEntity): + """Representation of a Meteo.Lt Current Conditions Sensor.""" + + def __init__(self, coordinator, nearest_place, config_entry): + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_name = ( + f"{config_entry.title} {nearest_place.name} - Current Conditions" + ) + self._attr_unique_id = f"{config_entry.entry_id}-sensor" + self._state = None + + @cached_property + def native_value(self): + """Return the value of the sensor.""" + return self.coordinator.data.current_conditions().temperature + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes.""" + current_conditions = self.coordinator.data.current_conditions() + LOGGER.debug("Current conditions: %s", current_conditions) + + return { + "native_temperature": current_conditions.temperature, + "native_apparent_temperature": current_conditions.apparent_temperature, + "native_wind_speed": current_conditions.wind_speed, + "native_wind_gust_speed": current_conditions.wind_gust_speed, + "wind_bearing": current_conditions.wind_bearing, + "cloud_coverage": current_conditions.cloud_coverage, + "native_pressure": current_conditions.pressure, + "humidity": current_conditions.humidity, + "native_precipitation": current_conditions.precipitation, + "condition": current_conditions.condition, + "native_temperature_unit": UnitOfTemperature.CELSIUS, + "native_wind_speed_unit": UnitOfSpeed.METERS_PER_SECOND, + "native_pressure_unit": UnitOfPressure.HPA, + "native_precipitation_unit": UnitOfPrecipitationDepth.MILLIMETERS, + } diff --git a/custom_components/meteo_lt/translations/en.json b/custom_components/meteo_lt/translations/en.json index 673105b..993d829 100644 --- a/custom_components/meteo_lt/translations/en.json +++ b/custom_components/meteo_lt/translations/en.json @@ -1,27 +1,27 @@ -{ - "title": "Meteo.Lt Integration", - "config": { - "step": { - "user": { - "title": "Configure Meteo.Lt", - "description": "Set up Meteo.Lt integration with your Home Assistant.", - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - } - }, - "reconfigure": { - "title": "Reconfigure Meteo.Lt", - "description": "Update your Meteo.Lt integration settings.", - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - } - }, - "reconfigure_confirm": { - "title": "Confirm Reconfiguration", - "description": "Are you sure you want to reconfigure the Meteo.Lt integration?" - } - } - } -} +{ + "title": "Meteo.Lt", + "config": { + "step": { + "user": { + "title": "Configure Meteo.Lt", + "description": "Set up Meteo.Lt integration with your Home Assistant.", + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + }, + "reconfigure": { + "title": "Reconfigure Meteo.Lt", + "description": "Update your Meteo.Lt integration settings.", + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + }, + "reconfigure_confirm": { + "title": "Confirm Reconfiguration", + "description": "Are you sure you want to reconfigure the Meteo.Lt integration?" + } + } + } +} diff --git a/custom_components/meteo_lt/translations/lt.json b/custom_components/meteo_lt/translations/lt.json new file mode 100644 index 0000000..21d8979 --- /dev/null +++ b/custom_components/meteo_lt/translations/lt.json @@ -0,0 +1,27 @@ +{ + "title": "Meteo.Lt", + "config": { + "step": { + "user": { + "title": "Konfigūruoti Meteo.Lt", + "description": "Nustatyti Meteo.Lt integraciją su Home Assistant.", + "data": { + "latitude": "Platuma (latitude)", + "longitude": "Ilguma (longitude)" + } + }, + "reconfigure": { + "title": "Perkonfigūruoti Meteo.Lt", + "description": "Atnaujinti Meteo.Lt integracijos nustatymus.", + "data": { + "latitude": "Platuma (latitude)", + "longitude": "Ilguma (longitude)" + } + }, + "reconfigure_confirm": { + "title": "Patvirtinti perkonfigūravimą", + "description": "Ar jūs tikrai norite perkonfigūruoti Meteo.Lt integraciją?" + } + } + } +} diff --git a/custom_components/meteo_lt/weather.py b/custom_components/meteo_lt/weather.py index 1eb0f3b..3f2f784 100644 --- a/custom_components/meteo_lt/weather.py +++ b/custom_components/meteo_lt/weather.py @@ -1,149 +1,169 @@ -"""weather.py""" - -# pylint: disable=unused-argument, abstract-method - -from functools import cached_property -from typing import List, Dict, Union -from homeassistant.components.weather import WeatherEntity, WeatherEntityFeature -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - UnitOfSpeed, - UnitOfTemperature, - UnitOfPressure, - UnitOfPrecipitationDepth, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, LOGGER - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): - """Set up Meteo.Lt weather based on a config entry.""" - LOGGER.debug("Setting up config entry") - coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"] - nearest_place = hass.data[DOMAIN][entry.entry_id]["nearest_place"] - async_add_entities([MeteoLtWeather(coordinator, nearest_place)], True) - - -class MeteoLtWeather(CoordinatorEntity, WeatherEntity): - """Meteo.lt WeatherEntity implementation""" - - def __init__(self, coordinator, nearest_place): - """__init__""" - super().__init__(coordinator) - self._name = f"Meteo.Lt {nearest_place.name}" - - @property - def name(self): - """Name""" - return self._name - - @cached_property - def native_temperature(self): - """Native temperature""" - return self.coordinator.data.forecast_timestamps[0].temperature - - @cached_property - def native_temperature_unit(self): - """Native temperature unit""" - return UnitOfTemperature.CELSIUS - - @cached_property - def humidity(self): - """Humidity""" - return self.coordinator.data.forecast_timestamps[0].humidity - - @cached_property - def native_wind_speed(self): - """Native wind speed""" - return self.coordinator.data.forecast_timestamps[0].wind_speed - - @cached_property - def native_wind_speed_unit(self): - """Native wind speed unit""" - return UnitOfSpeed.METERS_PER_SECOND - - @cached_property - def wind_bearing(self): - """Native wind bearing""" - return self.coordinator.data.forecast_timestamps[0].wind_bearing - - @cached_property - def native_pressure(self): - """Native pressure""" - return self.coordinator.data.forecast_timestamps[0].pressure - - @cached_property - def native_pressure_unit(self): - """Native pressure unit""" - return UnitOfPressure.HPA - - @cached_property - def native_precipitation(self): - """Native precipitation""" - return self.coordinator.data.forecast_timestamps[0].precipitation - - @cached_property - def native_precipitation_unit(self): - """Native precipitation unit""" - return UnitOfPrecipitationDepth.MILLIMETERS - - @cached_property - def condition(self): - """Condition""" - return self.coordinator.data.forecast_timestamps[0].condition - - @cached_property - def cloud_coverage(self): - """Cloud coverage""" - return self.coordinator.data.forecast_timestamps[0].cloud_coverage - - @cached_property - def native_apparent_temperature(self): - """Native apparent temperature""" - return self.coordinator.data.forecast_timestamps[0].apparent_temperature - - @cached_property - def native_wind_gust_speed(self): - """Native wind gust speed""" - return self.coordinator.data.forecast_timestamps[0].wind_gust_speed - - @property - def supported_features(self): - """Return the list of supported features.""" - return WeatherEntityFeature.FORECAST_HOURLY - - async def async_forecast_hourly( - self, - ) -> Union[List[Dict[str, Union[str, float]]], None]: - """Return the hourly forecast in native units.""" - if not self.supported_features & WeatherEntityFeature.FORECAST_HOURLY: - return None - - hourly_forecast = [ - { - "datetime": entry.datetime, - "native_temperature": entry.temperature, - "native_apparent_temperature": entry.apparent_temperature, - "native_wind_speed": entry.wind_speed, - "native_wind_gust_speed": entry.wind_gust_speed, - "wind_bearing": entry.wind_bearing, - "cloud_coverage": entry.cloud_coverage, - "native_pressure": entry.pressure, - "humidity": entry.humidity, - "native_precipitation": entry.precipitation, - "condition": entry.condition, - "native_temperature_unit": UnitOfTemperature.CELSIUS, - "native_wind_speed_unit":UnitOfSpeed.METERS_PER_SECOND, - "native_pressure_unit": UnitOfPressure.HPA, - "native_precipitation_unit": UnitOfPrecipitationDepth.MILLIMETERS, - } - for entry in self.coordinator.data.forecast_timestamps - ] - LOGGER.debug("Hourly_forecast created: %s", hourly_forecast) - return hourly_forecast - - async def async_update(self): - """Refreshing coordinator""" - LOGGER.debug("Updating MeteoLtWeather entity.") - await self.coordinator.async_request_refresh() +"""weather.py""" + +# pylint: disable=unused-argument, abstract-method + +from functools import cached_property +from typing import List, Dict, Union +from homeassistant.components.weather import WeatherEntity, WeatherEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + UnitOfSpeed, + UnitOfTemperature, + UnitOfPressure, + UnitOfPrecipitationDepth, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN, MANUFACTURER, LOGGER + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up Meteo.Lt weather based on a config entry.""" + LOGGER.debug( + "Weather setting up input: hass.data - %s, config entry - %s", + hass.data[DOMAIN][entry.entry_id], + entry, + ) + async_add_entities( + [ + MeteoLtWeather( + hass.data[DOMAIN][entry.entry_id]["coordinator"], + hass.data[DOMAIN][entry.entry_id]["nearest_place"], + entry, + ) + ], + True, + ) + + +class MeteoLtWeather(CoordinatorEntity, WeatherEntity): + """Meteo.lt WeatherEntity implementation""" + + def __init__(self, coordinator, nearest_place, config_entry): + """__init__""" + super().__init__(coordinator) + self._attr_name = f"{config_entry.title} {nearest_place.name}" + self._attr_unique_id = config_entry.entry_id + + @property + def device_info(self): + """device info""" + return { + "entry_type": DeviceEntryType.SERVICE, + "identifiers": {(DOMAIN, self._attr_unique_id)}, + "name": self._attr_name, + "manufacturer": MANUFACTURER, + } + + @cached_property + def native_temperature(self): + """Native temperature""" + return self.coordinator.data.forecast_timestamps[0].temperature + + @cached_property + def native_temperature_unit(self): + """Native temperature unit""" + return UnitOfTemperature.CELSIUS + + @cached_property + def humidity(self): + """Humidity""" + return self.coordinator.data.forecast_timestamps[0].humidity + + @cached_property + def native_wind_speed(self): + """Native wind speed""" + return self.coordinator.data.forecast_timestamps[0].wind_speed + + @cached_property + def native_wind_speed_unit(self): + """Native wind speed unit""" + return UnitOfSpeed.METERS_PER_SECOND + + @cached_property + def wind_bearing(self): + """Native wind bearing""" + return self.coordinator.data.forecast_timestamps[0].wind_bearing + + @cached_property + def native_pressure(self): + """Native pressure""" + return self.coordinator.data.forecast_timestamps[0].pressure + + @cached_property + def native_pressure_unit(self): + """Native pressure unit""" + return UnitOfPressure.HPA + + @cached_property + def native_precipitation(self): + """Native precipitation""" + return self.coordinator.data.forecast_timestamps[0].precipitation + + @cached_property + def native_precipitation_unit(self): + """Native precipitation unit""" + return UnitOfPrecipitationDepth.MILLIMETERS + + @cached_property + def condition(self): + """Condition""" + return self.coordinator.data.forecast_timestamps[0].condition + + @cached_property + def cloud_coverage(self): + """Cloud coverage""" + return self.coordinator.data.forecast_timestamps[0].cloud_coverage + + @cached_property + def native_apparent_temperature(self): + """Native apparent temperature""" + return self.coordinator.data.forecast_timestamps[0].apparent_temperature + + @cached_property + def native_wind_gust_speed(self): + """Native wind gust speed""" + return self.coordinator.data.forecast_timestamps[0].wind_gust_speed + + @property + def supported_features(self): + """Return the list of supported features.""" + return WeatherEntityFeature.FORECAST_HOURLY + + async def async_forecast_hourly( + self, + ) -> Union[List[Dict[str, Union[str, float]]], None]: + """Return the hourly forecast in native units.""" + if not self.supported_features & WeatherEntityFeature.FORECAST_HOURLY: + return None + + hourly_forecast = [ + { + "datetime": entry.datetime, + "native_temperature": entry.temperature, + "native_apparent_temperature": entry.apparent_temperature, + "native_wind_speed": entry.wind_speed, + "native_wind_gust_speed": entry.wind_gust_speed, + "wind_bearing": entry.wind_bearing, + "cloud_coverage": entry.cloud_coverage, + "native_pressure": entry.pressure, + "humidity": entry.humidity, + "native_precipitation": entry.precipitation, + "condition": entry.condition, + "native_temperature_unit": UnitOfTemperature.CELSIUS, + "native_wind_speed_unit": UnitOfSpeed.METERS_PER_SECOND, + "native_pressure_unit": UnitOfPressure.HPA, + "native_precipitation_unit": UnitOfPrecipitationDepth.MILLIMETERS, + } + for entry in self.coordinator.data.forecast_timestamps + ] + LOGGER.debug("Hourly_forecast created: %s", hourly_forecast) + return hourly_forecast + + async def async_update(self): + """Refreshing coordinator""" + LOGGER.debug("Updating MeteoLtWeather entity.") + await self.coordinator.async_request_refresh() diff --git a/hacs.json b/hacs.json index d6789bd..d15725e 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ -{ - "name": "Meteo.Lt Integration", - "homeassistant": "2024.7.3", - "render_readme": false +{ + "name": "Meteo.Lt Integration", + "homeassistant": "2024.7.3", + "render_readme": false } \ No newline at end of file diff --git a/images/icon.png b/images/icon.png index 7b7c767..de02bec 100644 Binary files a/images/icon.png and b/images/icon.png differ diff --git a/images/logo.png b/images/logo.png deleted file mode 100644 index 7b7c767..0000000 Binary files a/images/logo.png and /dev/null differ diff --git a/requirements.txt b/requirements.txt index 960898f..2306c82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ -colorlog==6.8.2 -homeassistant==2024.7.3 -pip>=24.1.1,<24.2 -meteo_lt-pkg +colorlog==6.8.2 +homeassistant==2024.7.3 +pip>=24.1.1,<24.2 +black +meteo_lt-pkg +ruff \ No newline at end of file