From 7df575d17c140e6afb145880cef9d954a1b01f37 Mon Sep 17 00:00:00 2001 From: Eugeniusz-Gienek Date: Wed, 1 Jan 2025 03:07:08 +0100 Subject: [PATCH] [weather.ha] 0.0.6.1b --- weather.ha/.gitignore | 171 +++++ weather.ha/LICENSE | 674 ++++++++++++++++++ weather.ha/README.md | 14 + weather.ha/addon.xml | 26 + weather.ha/changelog.txt | 0 weather.ha/default.py | 4 + weather.ha/plugin/__init__.py | 1 + weather.ha/plugin/_kodi_adapter.py | 60 ++ weather.ha/plugin/_plugin.py | 66 ++ weather.ha/plugin/util/__init__.py | 0 weather.ha/plugin/util/forecast_converter.py | 185 +++++ weather.ha/resources/banner.png | Bin 0 -> 20497 bytes weather.ha/resources/fanart.png | Bin 0 -> 28419 bytes weather.ha/resources/icon.png | Bin 0 -> 9289 bytes .../resource.language.en_gb/strings.po | 86 +++ .../resource.language.pl_pl/strings.po | 86 +++ weather.ha/resources/settings.xml | 17 + weather.ha/test/test_speed_units.py | 69 ++ weather.ha/test/test_temperature_units.py | 48 ++ 19 files changed, 1507 insertions(+) create mode 100644 weather.ha/.gitignore create mode 100644 weather.ha/LICENSE create mode 100644 weather.ha/README.md create mode 100644 weather.ha/addon.xml create mode 100644 weather.ha/changelog.txt create mode 100644 weather.ha/default.py create mode 100644 weather.ha/plugin/__init__.py create mode 100644 weather.ha/plugin/_kodi_adapter.py create mode 100644 weather.ha/plugin/_plugin.py create mode 100644 weather.ha/plugin/util/__init__.py create mode 100644 weather.ha/plugin/util/forecast_converter.py create mode 100644 weather.ha/resources/banner.png create mode 100644 weather.ha/resources/fanart.png create mode 100644 weather.ha/resources/icon.png create mode 100644 weather.ha/resources/language/resource.language.en_gb/strings.po create mode 100644 weather.ha/resources/language/resource.language.pl_pl/strings.po create mode 100644 weather.ha/resources/settings.xml create mode 100644 weather.ha/test/test_speed_units.py create mode 100644 weather.ha/test/test_temperature_units.py diff --git a/weather.ha/.gitignore b/weather.ha/.gitignore new file mode 100644 index 000000000..15201acc1 --- /dev/null +++ b/weather.ha/.gitignore @@ -0,0 +1,171 @@ +# 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 + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.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/ + +# PyPI configuration file +.pypirc diff --git a/weather.ha/LICENSE b/weather.ha/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/weather.ha/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/weather.ha/README.md b/weather.ha/README.md new file mode 100644 index 000000000..8a4431092 --- /dev/null +++ b/weather.ha/README.md @@ -0,0 +1,14 @@ +# KODI weather forecast from HomeAssistant + +## Instructions +In order to make it work there are a couple of actions which have to be performed on both HomeAssistant and Kodi. + +1. In Home Assistant you have to [create a token](https://community.home-assistant.io/t/how-to-get-long-lived-access-token/162159/5) for this plugin. + - get to your Profile page on Home Assistant by clicking on your username in the bottom left panel of the HA page (under Notifications) + - then click on Create Token all the way at the bottom + - give it a meaningful name and then copy it for use in your application. +2. In Kodi you have to do the following: + - Install plugin + - In settings put the long live token and url to your Home Assistant server + - Check the entity IDs + - Done. Enjoy :) diff --git a/weather.ha/addon.xml b/weather.ha/addon.xml new file mode 100644 index 000000000..6d0593788 --- /dev/null +++ b/weather.ha/addon.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + Weather forecast from Home Assistant + Weather forecast provided by Your Home Assistant server + Prognoza Pogody Home Assistant + Prognoza pogody od Twojego lokalnego serwera Home Assistant + all + en pl + GPL-3.0 + https://github.com/Eugeniusz-Gienek/kodi_weather_ha + + resources/icon.png + resources/fanart.png + resources/banner.png + + + diff --git a/weather.ha/changelog.txt b/weather.ha/changelog.txt new file mode 100644 index 000000000..e69de29bb diff --git a/weather.ha/default.py b/weather.ha/default.py new file mode 100644 index 000000000..89454f2d9 --- /dev/null +++ b/weather.ha/default.py @@ -0,0 +1,4 @@ +from plugin import KodiHomeAssistantWeatherPlugin + +if __name__ == '__main__': + KodiHomeAssistantWeatherPlugin() diff --git a/weather.ha/plugin/__init__.py b/weather.ha/plugin/__init__.py new file mode 100644 index 000000000..a38ed0275 --- /dev/null +++ b/weather.ha/plugin/__init__.py @@ -0,0 +1 @@ +from ._plugin import KodiHomeAssistantWeatherPlugin diff --git a/weather.ha/plugin/_kodi_adapter.py b/weather.ha/plugin/_kodi_adapter.py new file mode 100644 index 000000000..178ff847e --- /dev/null +++ b/weather.ha/plugin/_kodi_adapter.py @@ -0,0 +1,60 @@ +from enum import IntEnum + +from lib.kodi import KodiWeatherPluginAdapter, KodiPluginSetting + + +class _HomeAssistantWeatherPluginSettings(KodiPluginSetting): + LOCATION_TITLE = KodiPluginSetting(setting_id="loc_title", setting_type=str) + USE_HOME_ASSISTANT_LOCATION_NAME = KodiPluginSetting(setting_id="useHALocName", setting_type=bool) + HOME_ASSISTANT_SERVER = KodiPluginSetting(setting_id="ha_server", setting_type=str) + HOME_ASSISTANT_TOKEN = KodiPluginSetting(setting_id="ha_key", setting_type=str) + HOME_ASSISTANT_WEATHER_FORECAST_ENTITY_ID = KodiPluginSetting( + setting_id="ha_weather_forecast_entity_id", setting_type=str + ) + HOME_ASSISTANT_SUN_ENTITY_ID = KodiPluginSetting(setting_id="ha_sun_entity_id", setting_type=str) + LOG_ENABLED = KodiPluginSetting(setting_id="logEnabled", setting_type=bool) + + +class _HomeAssistantWeatherPluginStrings(IntEnum): + SETTINGS_REQUIRED = 30010 + HOMEASSISTANT_UNAUTHORIZED = 30011 + HOMEASSISTANT_UNREACHABLE = 30013 + HOMEASSISTANT_UNEXPECTED_RESPONSE = 30014 + ADDON_SHORT_NAME = 30200 + + +class _KodiHomeAssistantWeatherPluginAdapter(KodiWeatherPluginAdapter): + def __init__(self) -> None: + super().__init__() + self._allow_logging = self._get_setting(setting=_HomeAssistantWeatherPluginSettings.LOG_ENABLED) + + def required_settings_done(self) -> bool: + return ( + bool(self.home_assistant_token) + and bool(self.home_assistant_url) + and bool(self.home_assistant_entity_forecast) + and bool(self.home_assistant_entity_sun) + ) + + @property + def home_assistant_url(self) -> str: + return self._get_setting(setting=_HomeAssistantWeatherPluginSettings.HOME_ASSISTANT_SERVER) + + @property + def home_assistant_entity_forecast(self) -> str: + return self._get_setting(setting=_HomeAssistantWeatherPluginSettings.HOME_ASSISTANT_WEATHER_FORECAST_ENTITY_ID) + + @property + def home_assistant_entity_sun(self) -> str: + return self._get_setting(setting=_HomeAssistantWeatherPluginSettings.HOME_ASSISTANT_SUN_ENTITY_ID) + + @property + def home_assistant_token(self) -> str: + return self._get_setting(setting=_HomeAssistantWeatherPluginSettings.HOME_ASSISTANT_TOKEN) + + @property + def override_location(self) -> str: + if not self._get_setting(setting=_HomeAssistantWeatherPluginSettings.USE_HOME_ASSISTANT_LOCATION_NAME): + return self._get_setting(setting=_HomeAssistantWeatherPluginSettings.LOCATION_TITLE) + else: + return "" diff --git a/weather.ha/plugin/_plugin.py b/weather.ha/plugin/_plugin.py new file mode 100644 index 000000000..c9ed541cf --- /dev/null +++ b/weather.ha/plugin/_plugin.py @@ -0,0 +1,66 @@ +from typing import Tuple, Union + +from lib.homeassistant import HomeAssistantAdapter, RequestError, HomeAssistantForecast, HomeAssistantSunInfo +from lib.kodi import KodiLogLevel +from .util.forecast_converter import ForecastConverter +from ._kodi_adapter import _KodiHomeAssistantWeatherPluginAdapter, _HomeAssistantWeatherPluginStrings + + +class KodiHomeAssistantWeatherPlugin: + def __init__(self): + self._kodi_adapter = _KodiHomeAssistantWeatherPluginAdapter() + self._kodi_adapter.log("Home Assistant Weather started.") + + if not self._kodi_adapter.required_settings_done(): + self._kodi_adapter.dialog(message_id=_HomeAssistantWeatherPluginStrings.SETTINGS_REQUIRED) + self._kodi_adapter.log("Settings for Home Assistant Weather not yet provided. Plugin will not work.") + else: + self.apply_forecast() + self._kodi_adapter.log("Home Assistant Weather init finished.") + + def _get_forecast_handling_errors(self) \ + -> Tuple[Union[HomeAssistantForecast, None], Union[HomeAssistantSunInfo, None]]: + try: + return ( + HomeAssistantAdapter.get_forecast( + server_url=self._kodi_adapter.home_assistant_url, + entity_id=self._kodi_adapter.home_assistant_entity_forecast, + token=self._kodi_adapter.home_assistant_token + ), + HomeAssistantAdapter.get_sun_info( + server_url=self._kodi_adapter.home_assistant_url, + entity_id=self._kodi_adapter.home_assistant_entity_sun, + token=self._kodi_adapter.home_assistant_token + ) + ) + except RequestError as e: + self._kodi_adapter.log( + message=f"Could not retrieve forecast from Home Assistant: {e.error_code}", level=KodiLogLevel.ERROR + ) + if e.error_code == 401: + message = _HomeAssistantWeatherPluginStrings.HOMEASSISTANT_UNAUTHORIZED + elif e.error_code == -1: + message = _HomeAssistantWeatherPluginStrings.HOMEASSISTANT_UNREACHABLE + else: + message = _HomeAssistantWeatherPluginStrings.HOMEASSISTANT_UNEXPECTED_RESPONSE + self._kodi_adapter.dialog(message_id=message) + return None, None + + def apply_forecast(self): + forecast, sun_info = self._get_forecast_handling_errors() + if forecast is None: + self._kodi_adapter.log(message="No forecasts were found.", level=KodiLogLevel.WARNING) + self._kodi_adapter.clear_weather_properties() + return + if sun_info is None: + self._kodi_adapter.log(message="No sun info was found.", level=KodiLogLevel.WARNING) + self._kodi_adapter.clear_weather_properties() + return + kodi_forecast = ForecastConverter.translate_ha_forecast_to_kodi_forecast( + ha_forecast=forecast, + ha_sun_info=sun_info, + ) + if self._kodi_adapter.override_location: + kodi_forecast.General.location = self._kodi_adapter.override_location + self._kodi_adapter.set_weather_properties(forecast=kodi_forecast) + self._kodi_adapter.log(message="Weather updated successfully.", level=KodiLogLevel.INFO) diff --git a/weather.ha/plugin/util/__init__.py b/weather.ha/plugin/util/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/weather.ha/plugin/util/forecast_converter.py b/weather.ha/plugin/util/forecast_converter.py new file mode 100644 index 000000000..7ecce0cc3 --- /dev/null +++ b/weather.ha/plugin/util/forecast_converter.py @@ -0,0 +1,185 @@ +from datetime import datetime +from typing import Union + +from lib.homeassistant import ( + HomeAssistantHourlyForecast, HomeAssistantForecastMeta, HomeAssistantDailyForecast, + HomeAssistantForecast, HomeAssistantSunInfo, HomeAssistantWeatherCondition +) +from lib.kodi import ( + KodiHourlyForecastData, KodiWindDirectionCode, KodiDailyForecastData, KodiForecastData, + KodiGeneralForecastData, KodiCurrentForecastData, KodiConditionCode +) +from lib.unit.speed import SpeedUnits +from lib.unit.temperature import TemperatureUnits +from lib.util.thermal_comfort import ThermalComfort + + +class ForecastConverter: + @staticmethod + def __translate_hourly_ha_forecast_to_kodi_forecast( + ha_forecast: HomeAssistantHourlyForecast, forecast_meta: HomeAssistantForecastMeta, + sunset: datetime, sunrise: datetime + ) -> KodiHourlyForecastData: + temperature = TemperatureUnits[forecast_meta.temperature_unit](ha_forecast.temperature) + wind_speed = SpeedUnits[forecast_meta.wind_speed_unit](ha_forecast.wind_speed) + timestamp = ForecastConverter.__parse_homeassistant_datetime(datetime_str=ha_forecast.datetime) + return KodiHourlyForecastData( + temperature=temperature, + wind_speed=wind_speed, + wind_direction=KodiWindDirectionCode.from_bearing(bearing=ha_forecast.wind_bearing), + precipitation=ForecastConverter.__format_precipitation( + precipitation=ha_forecast.precipitation, precipitation_unit=forecast_meta.precipitation_unit + ), + humidity=ha_forecast.humidity, + feels_like=ThermalComfort.feels_like( + temperature=temperature, + wind_speed=wind_speed + ), + dew_point=ThermalComfort.dew_point( + temperature=temperature, + humidity_percent=ha_forecast.humidity + ), + condition=ForecastConverter.__translate_condition( + ha_condition=ha_forecast.condition, is_night=not (sunrise.time() < timestamp.time() < sunset.time()) + ), + timestamp=timestamp, + pressure="", + ) + + @staticmethod + def __translate_daily_ha_forecast_to_kodi_forecast( + ha_forecast: HomeAssistantDailyForecast, forecast_meta: HomeAssistantForecastMeta) -> KodiDailyForecastData: + temperature = TemperatureUnits[forecast_meta.temperature_unit](ha_forecast.temperature) + low_temperature = TemperatureUnits[forecast_meta.temperature_unit](ha_forecast.templow) + wind_speed = SpeedUnits[forecast_meta.wind_speed_unit](ha_forecast.wind_speed) + return KodiDailyForecastData( + temperature=temperature, + wind_speed=wind_speed, + wind_direction=KodiWindDirectionCode.from_bearing(bearing=ha_forecast.wind_bearing), + precipitation=ForecastConverter.__format_precipitation( + precipitation=ha_forecast.precipitation, precipitation_unit=forecast_meta.precipitation_unit + ), + condition=ForecastConverter.__translate_condition(ha_condition=ha_forecast.condition), + timestamp=ForecastConverter.__parse_homeassistant_datetime(datetime_str=ha_forecast.datetime), + low_temperature=low_temperature, + ) + + @staticmethod + def translate_ha_forecast_to_kodi_forecast( + ha_forecast: HomeAssistantForecast, ha_sun_info: HomeAssistantSunInfo) -> KodiForecastData: + temperature = TemperatureUnits[ha_forecast.current.temperature_unit](ha_forecast.current.temperature) + wind_speed = SpeedUnits[ha_forecast.current.wind_speed_unit](ha_forecast.current.wind_speed) + sunrise = ForecastConverter.__parse_homeassistant_datetime(ha_sun_info.next_rising) + sunset = ForecastConverter.__parse_homeassistant_datetime(ha_sun_info.next_setting) + return KodiForecastData( + General=KodiGeneralForecastData( + location=ha_forecast.current.friendly_name, + attribution=ha_forecast.current.attribution, + ), + Current=KodiCurrentForecastData( + temperature=temperature, + wind_speed=wind_speed, + wind_direction=KodiWindDirectionCode.from_bearing(bearing=ha_forecast.current.wind_bearing), + precipitation=ForecastConverter.__format_precipitation( + precipitation=ha_forecast.hourly[0].precipitation if len(ha_forecast.hourly) > 0 else None, + precipitation_unit=ha_forecast.current.precipitation_unit + ), # conversion not implemented in Kodi + condition=ForecastConverter.__translate_condition( + ha_condition=ha_forecast.hourly[0].condition if len(ha_forecast.hourly) > 0 else None, + is_night=not (sunrise.time() < datetime.now().time() < sunset.time()) + ), + humidity=ha_forecast.current.humidity, + feels_like=ThermalComfort.feels_like( + temperature=temperature, + wind_speed=wind_speed, + ), + dew_point=ThermalComfort.dew_point( + temperature=temperature, + humidity_percent=ha_forecast.current.humidity + ), + uv_index=int(ha_forecast.current.uv_index if ha_forecast.current.uv_index != None else 0), + cloudiness=int(ha_forecast.current.cloud_coverage if ha_forecast.current.cloud_coverage != None else 0), + pressure=ForecastConverter.__format_pressure( + pressure=ha_forecast.current.pressure if ha_forecast.current.pressure != None else 0, pressure_unit=ha_forecast.current.pressure_unit + ), + sunrise=sunrise, + sunset=sunset, + ), + HourlyForecasts=[ + ForecastConverter.__translate_hourly_ha_forecast_to_kodi_forecast( + ha_forecast=hourly_forecast, + forecast_meta=ha_forecast.current, + sunrise=sunrise, + sunset=sunset, + ) + for hourly_forecast in ha_forecast.hourly + ], + DailyForecasts=[ + ForecastConverter.__translate_daily_ha_forecast_to_kodi_forecast( + ha_forecast=daily_forecast, + forecast_meta=ha_forecast.current + ) + for daily_forecast in ha_forecast.daily + ] + ) + + @staticmethod + def __translate_condition( + ha_condition: Union[HomeAssistantWeatherCondition, None], is_night: bool = False + ) -> Union[KodiConditionCode, None]: + if ha_condition is None: + return None + elif ha_condition == HomeAssistantWeatherCondition.CLEAR_NIGHT.value: + return KodiConditionCode.CLEAR_NIGHT + elif ha_condition == HomeAssistantWeatherCondition.CLOUDY.value: + return KodiConditionCode.CLOUDY if not is_night else KodiConditionCode.MOSTLY_CLOUDY_NIGHT + elif ha_condition == HomeAssistantWeatherCondition.FOG.value: + return KodiConditionCode.FOGGY + elif ha_condition == HomeAssistantWeatherCondition.HAIL.value: + return KodiConditionCode.HAIL + elif ha_condition == HomeAssistantWeatherCondition.LIGHTNING.value: + return KodiConditionCode.THUNDERSTORMS + elif ha_condition == HomeAssistantWeatherCondition.LIGHTNING_RAINY.value: + return KodiConditionCode.THUNDERSHOWERS + elif ha_condition == HomeAssistantWeatherCondition.PARTLY_CLOUDY.value: + return KodiConditionCode.PARTLY_CLOUDY if not is_night else KodiConditionCode.PARTLY_CLOUDY_NIGHT + elif ha_condition == HomeAssistantWeatherCondition.POURING.value: + return KodiConditionCode.SHOWERS_2 + elif ha_condition == HomeAssistantWeatherCondition.RAINY.value: + return KodiConditionCode.SHOWERS + elif ha_condition == HomeAssistantWeatherCondition.SNOWY.value: + return KodiConditionCode.SNOW + elif ha_condition == HomeAssistantWeatherCondition.SNOWY_RAINY.value: + return KodiConditionCode.MIXED_RAIN_AND_SNOW + elif ha_condition == HomeAssistantWeatherCondition.SUNNY.value: + return KodiConditionCode.SUNNY + elif ha_condition == HomeAssistantWeatherCondition.WINDY.value: + return KodiConditionCode.WINDY + elif ha_condition == HomeAssistantWeatherCondition.WINDY_CLOUDY.value: + return KodiConditionCode.WINDY + elif ha_condition == HomeAssistantWeatherCondition.EXCEPTIONAL.value: + return KodiConditionCode.SEVERE_THUNDERSTORMS + else: + raise ValueError(f"Unknown condition: {ha_condition}") + + @staticmethod + def __format_precipitation(precipitation: Union[float, None], precipitation_unit: str) -> Union[str, None]: + if precipitation is None: + return None + # scientific rounding to 0 or 1 significant decimal + if precipitation > 3: + fmt = "{:.0f} {}" + else: + fmt = "{:.1f} {}" + return fmt.format(precipitation, precipitation_unit) + + @staticmethod + def __format_pressure(pressure: Union[float, None], pressure_unit: str) -> str: + if pressure is None: + return "" + return "{:.0f} {}".format(pressure, pressure_unit) + + @staticmethod + def __parse_homeassistant_datetime(datetime_str: str) -> datetime: + # tz=None adds time offset to match Kodi's set time + return datetime.fromisoformat(datetime_str).astimezone(tz=None) diff --git a/weather.ha/resources/banner.png b/weather.ha/resources/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..5cb7b16f9cc212782f26b69753fda52bd9811ebc GIT binary patch literal 20497 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�f>aIbLZZzM<|-hG=8!McyLkcXWWcDA36 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 DP;SOL literal 0 HcmV?d00001 diff --git a/weather.ha/resources/fanart.png b/weather.ha/resources/fanart.png new file mode 100644 index 0000000000000000000000000000000000000000..06519c3e0df639b74bc2b6dcfe938df7cd6fd251 GIT binary patch literal 28419 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 E0Vs9rE&u=k literal 0 HcmV?d00001 diff --git a/weather.ha/resources/icon.png b/weather.ha/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..519bb771b9677827beecfd0567939ac3367a6751 GIT binary patch literal 9289 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 RBE$|D>Ka`r(YAT`e*gr-bp8MU literal 0 HcmV?d00001 diff --git a/weather.ha/resources/language/resource.language.en_gb/strings.po b/weather.ha/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 000000000..e29b3f8b6 --- /dev/null +++ b/weather.ha/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,86 @@ +# Kodi Media Center language file +# Addon Name: Home Assistant Weather +# Addon id: weather.ha +# Addon Provider: eugeniusz.gienek +msgid "" +msgstr "" +"Project-Id-Version: Kodi add-ons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2024-12-24 01:35+0100\n" +"Last-Translator: Konstantin Koehring <-@-.pl>\n" +"Language-Team: British \n" +"Language: en_gb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n=1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.10.1\n" + +msgctxt "Addon Summary" +msgid "Home Assistant Weather forecast" +msgstr "" + +msgctxt "Addon Description" +msgid "Weather forecast provided by Home Assistant" +msgstr "" + +msgctxt "#30001" +msgid "Connection" +msgstr "" + +msgctxt "#30002" +msgid "Home Assistant Server URL (with protocol)" +msgstr "" + +msgctxt "#30003" +msgid "Home Assistant Long Live Token" +msgstr "" + +msgctxt "#30004" +msgid "Home Assistant Weather Forecast Entity ID" +msgstr "" + +msgctxt "#30005" +msgid "Home Assistant Sun Entity ID" +msgstr "" + +msgctxt "#30008" +msgid "Debug" +msgstr "" + +msgctxt "#30009" +msgid "Enable logging" +msgstr "" + +msgctxt "#30010" +msgid "Please provide settings for the Home Assistant Weather plugin - it won't work without it." +msgstr "" + +msgctxt "#30011" +msgid "Please check your token - it seems to be not accepted." +msgstr "" + +msgctxt "#30013" +msgid "Some unknown error happened. Please check IP address or if server is even online." +msgstr "" + +msgctxt "#30014" +msgid "Unexpected response from Home Assistant weather server. This is an error." +msgstr "" + +msgctxt "#30018" +msgid "General" +msgstr "" + +msgctxt "#30019" +msgid "Location title displayed" +msgstr "" + +msgctxt "#30020" +msgid "Use HA location name" +msgstr "" + +msgctxt "#30200" +msgid "Home Assistant Weather" +msgstr "" diff --git a/weather.ha/resources/language/resource.language.pl_pl/strings.po b/weather.ha/resources/language/resource.language.pl_pl/strings.po new file mode 100644 index 000000000..360fedaf1 --- /dev/null +++ b/weather.ha/resources/language/resource.language.pl_pl/strings.po @@ -0,0 +1,86 @@ +# Kodi Media Center language file +# Addon Name: Home Assistant Weather +# Addon id: weather.ha +# Addon Provider: eugeniusz.gienek +msgid "" +msgstr "" +"Project-Id-Version: Kodi add-ons\n" +"Report-Msgid-Bugs-To: translations@kodi.tv\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2024-12-29 01:35+0100\n" +"Last-Translator: Konstantin Koehring <-@-.pl>\n" +"Language-Team: Polish \n" +"Language: pl_pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.10.1\n" + +msgctxt "Addon Summary" +msgid "Home Assistant Weather forecast" +msgstr "Prognoza pogody Home Assistant" + +msgctxt "Addon Description" +msgid "Weather forecast provided by Home Assistant" +msgstr "Prognoza pogody dostarczona przez Home Assistant" + +msgctxt "#30001" +msgid "Connection" +msgstr "Połączenie" + +msgctxt "#30002" +msgid "Home Assistant Server URL (with protocol)" +msgstr "URL Serwera Home Assistant (z protokołem)" + +msgctxt "#30003" +msgid "Home Assistant Long Live Token" +msgstr "Home Assistant Long Live Token" + +msgctxt "#30004" +msgid "Home Assistant Weather Forecast Entity ID" +msgstr "Identyfikator jednostki prognozy pogody Home Assistant" + +msgctxt "#30005" +msgid "Home Assistant Sun Entity ID" +msgstr "Identyfikator jednostki słońca Home Assistant" + +msgctxt "#30008" +msgid "Debug" +msgstr "Debug" + +msgctxt "#30009" +msgid "Enable logging" +msgstr "Włącz logowanie" + +msgctxt "#30010" +msgid "Please provide settings for the Home Assistant Weather plugin - it won't work without it." +msgstr "Żeby wtyczka Pogody Home Assistant działała poprawnie musisz podać dane w ustawieniach." + +msgctxt "#30011" +msgid "Please check your token - it seems to be not accepted." +msgstr "Wygląda na to że token nie był zaapceptowany." + +msgctxt "#30013" +msgid "Some unknown error happened. Please check IP address or if server is even online." +msgstr "Coś się stało - warto sprawdzić czy adres URL/IP jest poprawny no i czy serwer jest włączony." + +msgctxt "#30014" +msgid "Unexpected response from Home Assistant weather server. This is an error." +msgstr "Dostaliśmy nieoczeniwaną odpowiedź od serwera Home Assistant - to jest błąd." + +msgctxt "#30018" +msgid "General" +msgstr "Ogólne" + +msgctxt "#30019" +msgid "Location title displayed" +msgstr "Wyświetlana nazwa lokacji" + +msgctxt "#30020" +msgid "Use HA location name" +msgstr "Używaj nazwy lokacji z HA" + +msgctxt "#30200" +msgid "Home Assistant Weather" +msgstr "" diff --git a/weather.ha/resources/settings.xml b/weather.ha/resources/settings.xml new file mode 100644 index 000000000..124dfb1c0 --- /dev/null +++ b/weather.ha/resources/settings.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/weather.ha/test/test_speed_units.py b/weather.ha/test/test_speed_units.py new file mode 100644 index 000000000..1614974dd --- /dev/null +++ b/weather.ha/test/test_speed_units.py @@ -0,0 +1,69 @@ +import unittest +from typing import Mapping, Type + +from lib.unit.speed import ( + SpeedUnits, SpeedBft, SpeedKph, SpeedMpmin, SpeedMps, SpeedFtph, SpeedFtpm, SpeedFtps, SpeedMph, SpeedKts, + SpeedInps, SpeedYdps, SpeedFpf, Speed +) + +SAMPLE = 42.0 + +CONVERT_42SI_TO: Mapping[Type[Speed], float] = { + SpeedKph: 151.2, + SpeedMpmin: 2520.0, + SpeedMps: 42.0, + SpeedFtph: 496063.0, + SpeedFtpm: 8268.0, + SpeedFtps: 137.8, + SpeedMph: 93.95, + SpeedKts: 81.64, + SpeedBft: 12, + SpeedInps: 1653.54, + SpeedYdps: 45.93, + SpeedFpf: 252541.0, +} + + +class TestSpeedUnits(unittest.TestCase): + def test_unit_identity_via_from_si(self): + for unit in SpeedUnits.values(): + if unit == SpeedBft: + with self.subTest(msg=unit.unit): + self.assertEqual(10, unit.from_si_value(unit(10.0).si_value()).value) + else: + with self.subTest(msg=unit.unit): + self.assertAlmostEqual(SAMPLE, unit.from_si_value(unit(SAMPLE).si_value()).value) + + def test_si_identity_via_unit(self): + for unit in SpeedUnits.values(): + if unit == SpeedBft: + with self.subTest(msg=unit.unit): + # i hope bft scale won't change ;) + self.assertAlmostEqual(0.3, float(unit.from_si_value(0.3).si_value())) + else: + with self.subTest(msg=unit.unit): + self.assertAlmostEqual(SAMPLE, unit.from_si_value(SAMPLE).si_value()) + + def test_unit_from_si(self): + for unit in SpeedUnits.values(): + if unit == SpeedBft: + with self.subTest(msg=unit.unit): + self.assertEqual(CONVERT_42SI_TO[unit], unit.from_si_value(SAMPLE).value) + else: + with self.subTest(msg=unit.unit): + self.assertLess( + abs(CONVERT_42SI_TO[unit] - unit.from_si_value(SAMPLE).value) / CONVERT_42SI_TO[unit], 1e-4 + ) + + def test_si_from_unit(self): + for unit in SpeedUnits.values(): + if unit == SpeedBft: + # this test would make even less sense than the last ones + continue + else: + with self.subTest(msg=unit.unit): + self.assertLess(abs(SAMPLE - unit(CONVERT_42SI_TO[unit]).si_value()) / SAMPLE, 1e-4) + + +if __name__ == '__main__': + unittest.main() diff --git a/weather.ha/test/test_temperature_units.py b/weather.ha/test/test_temperature_units.py new file mode 100644 index 000000000..bb375adbe --- /dev/null +++ b/weather.ha/test/test_temperature_units.py @@ -0,0 +1,48 @@ +import unittest +from typing import Mapping, Type + +from lib.unit.temperature import ( + Temperature, TemperatureCelsius, TemperatureFahrenheit, TemperatureKelvin, TemperatureReaumur, TemperatureRankine, + TemperatureRomer, TemperatureDelisle, TemperatureNewton, TemperatureUnits +) + +SAMPLE = 345.67 + +CONVERT_42SI_TO: Mapping[Type[Temperature], float] = { + TemperatureCelsius: 72.52, + TemperatureFahrenheit: 162.5, + TemperatureKelvin: 345.67, + TemperatureReaumur: 58.02, + TemperatureRankine: 622.2, + TemperatureRomer: 45.57, + TemperatureDelisle: 41.2, + TemperatureNewton: 23.93, +} + + +class TestTemperatureUnits(unittest.TestCase): + def test_unit_identity_via_from_si(self): + for unit in TemperatureUnits.values(): + with self.subTest(msg=unit.unit): + self.assertAlmostEqual(SAMPLE, unit.from_si_value(unit(SAMPLE).si_value()).value) + + def test_si_identity_via_unit(self): + for unit in TemperatureUnits.values(): + with self.subTest(msg=unit.unit): + self.assertAlmostEqual(SAMPLE, unit.from_si_value(SAMPLE).si_value()) + + def test_unit_from_si(self): + for unit in TemperatureUnits.values(): + with self.subTest(msg=unit.unit): + self.assertLess( + abs(CONVERT_42SI_TO[unit] - unit.from_si_value(SAMPLE).value) / CONVERT_42SI_TO[unit], 1e-3 + ) + + def test_si_from_unit(self): + for unit in TemperatureUnits.values(): + with self.subTest(msg=unit.unit): + self.assertLess(abs(SAMPLE - unit(CONVERT_42SI_TO[unit]).si_value()) / SAMPLE, 1e-3) + + +if __name__ == '__main__': + unittest.main()