├── screenshots ├── ims.png ├── add_the_repo.png ├── repo_added.png ├── select-brand.png ├── ims_attributes.png ├── add-integration.png ├── hacs_integration.png ├── settings-devices.png ├── submit-settings.png ├── add_the_integration.png ├── add_custom_repositories.png └── add_custom_repositories.pmg_2.png ├── hacs.json ├── custom_components └── ims │ ├── services.yaml │ ├── manifest.json │ ├── utils.py │ ├── weather_update_coordinator.py │ ├── binary_sensor.py │ ├── translations │ ├── he.json │ ├── en.json │ └── pt.json │ ├── const.py │ ├── __init__.py │ ├── weather.py │ ├── config_flow.py │ └── sensor.py ├── .github ├── workflows │ ├── hassfest.yml │ └── hacs.yml └── FUNDING.yml ├── CHANGELOG.md ├── renovate.json ├── LICENSE ├── CODE_OF_CONDUCT.md └── README.md /screenshots/ims.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/ims.png -------------------------------------------------------------------------------- /screenshots/add_the_repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/add_the_repo.png -------------------------------------------------------------------------------- /screenshots/repo_added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/repo_added.png -------------------------------------------------------------------------------- /screenshots/select-brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/select-brand.png -------------------------------------------------------------------------------- /screenshots/ims_attributes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/ims_attributes.png -------------------------------------------------------------------------------- /screenshots/add-integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/add-integration.png -------------------------------------------------------------------------------- /screenshots/hacs_integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/hacs_integration.png -------------------------------------------------------------------------------- /screenshots/settings-devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/settings-devices.png -------------------------------------------------------------------------------- /screenshots/submit-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/submit-settings.png -------------------------------------------------------------------------------- /screenshots/add_the_integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/add_the_integration.png -------------------------------------------------------------------------------- /screenshots/add_custom_repositories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/add_custom_repositories.png -------------------------------------------------------------------------------- /screenshots/add_custom_repositories.pmg_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/ims-custom-component/HEAD/screenshots/add_custom_repositories.pmg_2.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Israel Meteorological Service (IMS)", 3 | "render_readme": true, 4 | "country": "IL", 5 | "homeassistant": "2025.4.0" 6 | } 7 | -------------------------------------------------------------------------------- /custom_components/ims/services.yaml: -------------------------------------------------------------------------------- 1 | debug_get_coordinator_data: 2 | description: "Fetch and return the coordinator data for debugging purposes." 3 | fields: {} -------------------------------------------------------------------------------- /.github/workflows/hassfest.yml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | jobs: 11 | validate: 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - uses: "actions/checkout@v6" 15 | - uses: home-assistant/actions/hassfest@master 16 | -------------------------------------------------------------------------------- /.github/workflows/hacs.yml: -------------------------------------------------------------------------------- 1 | name: HACS Validation 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | 11 | jobs: 12 | validate: 13 | runs-on: "ubuntu-latest" 14 | steps: 15 | - uses: "actions/checkout@v6" 16 | - name: HACS validation 17 | uses: "hacs/action@main" 18 | with: 19 | category: "integration" 20 | -------------------------------------------------------------------------------- /custom_components/ims/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "ims", 3 | "name": "The Israel Meteorological Service integration", 4 | "codeowners": ["@GuyKh", "@t0mer"], 5 | "config_flow": true, 6 | "documentation": "https://github.com/GuyKh/ims-custom-component", 7 | "iot_class": "cloud_polling", 8 | "issue_tracker": "https://github.com/GuyKh/ims-custom-component/issues", 9 | "requirements": ["weatheril>=0.40.2"], 10 | "version": "0.1.40" 11 | } -------------------------------------------------------------------------------- /custom_components/ims/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | night_weather_codes = ["1220", "1250"] 4 | 5 | 6 | def get_hourly_weather_icon(hour, weather_code, strptime="%H:%M"): 7 | hourly_weather_code = weather_code 8 | time_object = datetime.strptime(hour, strptime) 9 | if _is_night(time_object.hour) and hourly_weather_code in night_weather_codes: 10 | hourly_weather_code = hourly_weather_code + "-night" 11 | 12 | return hourly_weather_code 13 | 14 | 15 | def _is_night(hour): 16 | return hour < 6 or hour > 20 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.1.24 4 | 5 | * Update icon mapping with latest weather conditions 6 | 7 | ## v0.1.23 8 | 9 | * Another Fix for Monitored Condition backward compatibility 10 | 11 | ## v0.1.22 12 | 13 | * Fix Monitored Condition backward compatibility 14 | 15 | 16 | ## v0.1.21 17 | 18 | - [BREAKING CHANGE]: Rename `sensor.ims_rain` sensor to `sensor.ims_is_raining` 19 | - Add Precipitation and Precipitation Chance sensors 20 | - Fix _deprecated constant_ usage and log warning 21 | - Code styling according to standards 22 | - Fix gaps in hourly missing data in daily sensors 23 | - In ConfigFlow - select which sensors to create 24 | 25 | ## v0.1.20 26 | 27 | _Minimum HA Version: 2024.1.0b0_ 28 | 29 | 30 | #### Adjust code to HA 2024.1.0: 31 | 32 | - Fix usage of Constants according to deprecation 33 | - Fix Metaclass error, failing to create ImsSensor 34 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "automerge": true, 7 | "packageRules": [ 8 | { 9 | "matchDatasources": ["pypi"], 10 | "matchPackageNames": ["homeassistant"], 11 | "enabled": false 12 | }, 13 | { 14 | "matchDatasources": ["pypi"], 15 | "matchPaths": ["requirements.txt"], 16 | "automerge": true 17 | } 18 | ], 19 | "customManagers": [ 20 | { 21 | "customType": "regex", 22 | "fileMatch": ["^custom_components/.+/manifest\\.json$"], 23 | "matchStrings": ["\"requirements\":\\s*\\[(\\s*\"(?[^\"]+)(?(==|>=|<=))(?[0-9\\.]+)\"[,\\s]?)*\\]"], 24 | "versioningTemplate": "python", 25 | "depNameTemplate": "{{depName}}", 26 | "datasourceTemplate": "pypi" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: guykh # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: guykh # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tomer Klein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /custom_components/ims/weather_update_coordinator.py: -------------------------------------------------------------------------------- 1 | """Weather data coordinator for the OpenWeatherMap (OWM) service.""" 2 | 3 | import asyncio 4 | import datetime 5 | import logging 6 | import homeassistant.util.dt as dt_util 7 | 8 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 9 | from weatheril import WeatherIL, Forecast, Weather, RadarSatellite, Warning 10 | 11 | from .const import ( 12 | DOMAIN, 13 | IMS_TIMEZONE, 14 | ) 15 | from dataclasses import dataclass 16 | 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | ATTRIBUTION = "Powered by IMS Weather" 20 | 21 | # Use the shared timezone constant 22 | timezone = IMS_TIMEZONE 23 | 24 | 25 | class WeatherUpdateCoordinator(DataUpdateCoordinator): 26 | """Weather data update coordinator.""" 27 | 28 | def __init__(self, city, language, update_interval, hass): 29 | """Initialize coordinator.""" 30 | self.city = city 31 | self.language = language 32 | self.update_interval = update_interval 33 | self.weather = WeatherIL(str(city), language) 34 | 35 | self.data = None 36 | self._connect_error = False 37 | self._hass = hass 38 | 39 | super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) 40 | 41 | async def _async_update_data(self): 42 | """Update the data.""" 43 | data = {} 44 | async with self._hass.timeout.async_timeout(30): 45 | try: 46 | _LOGGER.info("Fetching data from IMS") 47 | data = await self._get_ims_weather() 48 | except Exception as error: 49 | raise UpdateFailed(error) from error 50 | return data 51 | 52 | async def _get_ims_weather(self): 53 | """Poll weather data from IMS.""" 54 | 55 | try: 56 | loop = asyncio.get_event_loop() 57 | except Exception: 58 | loop = asyncio.new_event_loop() 59 | 60 | current_weather = await loop.run_in_executor( 61 | None, self.weather.get_current_analysis 62 | ) 63 | weather_forecast = await loop.run_in_executor(None, self.weather.get_forecast) 64 | warnings = await loop.run_in_executor(None, self.weather.get_warnings) 65 | images = await loop.run_in_executor(None, self.weather.get_radar_images) 66 | 67 | _LOGGER.debug( 68 | "Data fetched from IMS of %s", 69 | current_weather.forecast_time.strftime("%m/%d/%Y, %H:%M:%S"), 70 | ) 71 | 72 | self._filter_future_forecast(weather_forecast) 73 | return WeatherData(current_weather, weather_forecast, images, warnings) 74 | 75 | @staticmethod 76 | def _filter_future_forecast(weather_forecast): 77 | """Filter Forecast to include only future dates""" 78 | today_datetime = dt_util.as_local( 79 | datetime.datetime.combine(dt_util.now(timezone).date(), datetime.time()) 80 | ) 81 | filtered_day_list = list( 82 | filter(lambda daily: daily.date >= today_datetime, weather_forecast.days) 83 | ) 84 | 85 | for daily_forecast in filtered_day_list: 86 | filtered_hours = [] 87 | for hourly_forecast in daily_forecast.hours: 88 | forecast_datetime = daily_forecast.date + datetime.timedelta( 89 | hours=int(hourly_forecast.hour.split(":")[0]) 90 | ) 91 | if dt_util.now(timezone) <= forecast_datetime: 92 | filtered_hours.append(hourly_forecast) 93 | daily_forecast.hours = filtered_hours 94 | 95 | weather_forecast.days = filtered_day_list 96 | 97 | 98 | @dataclass 99 | class WeatherData: 100 | current_weather: Weather 101 | forecast: Forecast 102 | images: RadarSatellite 103 | warnings: list[Warning] 104 | -------------------------------------------------------------------------------- /custom_components/ims/binary_sensor.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from dataclasses import dataclass 3 | 4 | from homeassistant.components.binary_sensor import ( 5 | BinarySensorEntityDescription, 6 | BinarySensorEntity, 7 | BinarySensorDeviceClass, 8 | ) 9 | from homeassistant.config_entries import ConfigEntry 10 | from homeassistant.const import CONF_MONITORED_CONDITIONS 11 | from homeassistant.core import HomeAssistant, callback 12 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 13 | import homeassistant.util.dt as dt_util 14 | 15 | from . import ImsEntity, ImsSensorEntityDescription 16 | from .const import ( 17 | TYPE_IS_RAINING, 18 | IMS_SENSOR_KEY_PREFIX, 19 | FORECAST_MODE, 20 | FIELD_NAME_RAIN, 21 | DOMAIN, 22 | ENTRY_WEATHER_COORDINATOR, 23 | TYPE_IS_ACTIVE_WEATHER_WARNING, 24 | FIELD_NAME_WARNING, 25 | IMS_TIMEZONE, 26 | ) 27 | from .weather_update_coordinator import WeatherData 28 | 29 | 30 | @dataclass(frozen=True, kw_only=True) 31 | class ImsBinaryEntityDescriptionMixin: 32 | """Mixin values for required keys.""" 33 | 34 | value_fn: Callable[[WeatherData], bool | None] 35 | 36 | 37 | @dataclass(frozen=True, kw_only=True) 38 | class ImsBinarySensorEntityDescription( 39 | ImsSensorEntityDescription, 40 | BinarySensorEntityDescription, 41 | ImsBinaryEntityDescriptionMixin, 42 | ): 43 | """Class describing IMS Binary sensors entities""" 44 | 45 | 46 | BINARY_SENSORS_DESCRIPTIONS: tuple[ImsBinarySensorEntityDescription, ...] = ( 47 | ImsBinarySensorEntityDescription( 48 | key=IMS_SENSOR_KEY_PREFIX + TYPE_IS_RAINING, 49 | name="IMS Is Raining", 50 | icon="mdi:weather-rainy", 51 | forecast_mode=FORECAST_MODE.CURRENT, 52 | field_name=FIELD_NAME_RAIN, 53 | value_fn=lambda data: data.current_weather.rain 54 | and data.current_weather.rain > 0.0, 55 | ), 56 | ImsBinarySensorEntityDescription( 57 | key=IMS_SENSOR_KEY_PREFIX + TYPE_IS_ACTIVE_WEATHER_WARNING, 58 | name="IMS Is Active Weather Warning", 59 | icon="mdi:weather-sunny-alert", 60 | device_class=BinarySensorDeviceClass.SAFETY, 61 | forecast_mode=FORECAST_MODE.CURRENT, 62 | field_name=FIELD_NAME_WARNING, 63 | value_fn=lambda data: any( 64 | warning.valid_from <= dt_util.now(IMS_TIMEZONE) <= warning.valid_to 65 | for warning in data.warnings 66 | ), 67 | ), 68 | ) 69 | 70 | BINARY_SENSOR_DESCRIPTIONS_DICT = { 71 | desc.key: desc for desc in BINARY_SENSORS_DESCRIPTIONS 72 | } 73 | BINARY_SENSOR_DESCRIPTIONS_KEYS = [desc.key for desc in BINARY_SENSORS_DESCRIPTIONS] 74 | 75 | 76 | async def async_setup_entry( 77 | hass: HomeAssistant, 78 | entry: ConfigEntry, 79 | async_add_entities: AddEntitiesCallback, 80 | ) -> None: 81 | """Set up a IMS binary sensors based on a config entry.""" 82 | domain_data = hass.data[DOMAIN][entry.entry_id] 83 | conditions = domain_data[CONF_MONITORED_CONDITIONS] 84 | weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] 85 | 86 | # Add IMS Sensors 87 | sensors: list[ImsBinarySensor] = [] 88 | 89 | if conditions is None: 90 | # If a problem happens - create all sensors 91 | conditions = BINARY_SENSOR_DESCRIPTIONS_KEYS 92 | 93 | for condition in conditions: 94 | if condition in BINARY_SENSOR_DESCRIPTIONS_KEYS: 95 | description = BINARY_SENSOR_DESCRIPTIONS_DICT[condition] 96 | sensors.append(ImsBinarySensor(weather_coordinator, description)) 97 | 98 | async_add_entities(sensors, update_before_add=True) 99 | 100 | 101 | class ImsBinarySensor(ImsEntity, BinarySensorEntity): 102 | """Defines an IMS binary sensor.""" 103 | 104 | @callback 105 | def _update_from_latest_data(self) -> None: 106 | """Update the state.""" 107 | self._attr_is_on = self.entity_description.value_fn(self.coordinator.data) 108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /custom_components/ims/translations/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "מוגדר חיישן עבור ישוב זה" 5 | }, 6 | "error": { 7 | "cannot_connect": "שגיאה בהתחברות" 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "city": "יישוב", 13 | "language": "שפה", 14 | "mode": "מצב תחזית עבור חיישן התחזית", 15 | "name": "שם האינטגרציה", 16 | "images_path": "הנתיב אליו יורדו התמונות", 17 | "update_interval": "מס' הדקות בין כל משיכת עדכון. לא מומלץ לשים ערך קטן מ-15 דק'.", 18 | "ims_platform": "ישות תחזית מזג אוויר ו/או יישות חיישן. בחירה בחיישן תייצר יישות עבור כל אחד מהמאפיינים בנפרד. אם יש ספק, בחרו במזג אוויר!", 19 | "monitored_conditions": "תנאים ליצור חיישנים עבורם, רק כאשר בחרת ליצור חיישנים" 20 | }, 21 | "description": "הגדרות שילוב השירות המטאורולוגי הישראלי", 22 | "data_description": {} 23 | } 24 | } 25 | }, 26 | "options": { 27 | "step": { 28 | "init": { 29 | "data": { 30 | "city": "יישוב", 31 | "language": "שפה", 32 | "mode": "מצב תחזית עבור חיישן התחזית", 33 | "name": "שם האינטגרציה", 34 | "images_path": "הנתיב אליו יורדו התמונות", 35 | "update_interval": "מס' הדקות בין כל משיכת עדכון. לא מומלץ לשים ערך קטן מ-15 דק'.", 36 | "ims_platform": "ישות תחזית מזג אוויר ו/או יישות חיישן. בחירה בחיישן תייצר יישות עבור כל אחד מהמאפיינים בנפרד. אם יש ספק, בחרו במזג אוויר!", 37 | "monitored_conditions": "תנאים ליצור חיישנים עבורם, רק כאשר בחרת ליצור חיישנים. \n הערה: הסרת חיישנים מהרשימה תיצור חיישנים יתומים שצריך להסיר ידנית" 38 | }, 39 | "description": "הגדרות שילוב השירות המטאורולוגי הישראלי", 40 | "data_description": {} 41 | } 42 | } 43 | }, 44 | "services": { 45 | "debug_get_coordinator_data": { 46 | "name": "הבא מידע שנטען מהשירות המטאורולוגי", 47 | "description": "הדפס מידע במערכת שנטען מהשירות המטאורולוגי לצורך ניפוי שגיאות." 48 | } 49 | }, 50 | "entity": { 51 | "binary_sensor": { 52 | "ims_is_raining_he": { 53 | "name": "גשם", 54 | "state": { 55 | "on": "יורד", 56 | "off": "לא יורד" 57 | } 58 | }, 59 | "ims_is_raining_en": { 60 | "name": "Rain", 61 | "state": { 62 | "on": "Raining", 63 | "off": "Not Raining" 64 | } 65 | }, 66 | "ims_is_active_weather_warning_he": { 67 | "name": "יש התראת מז\"א בתוקף", 68 | "state": { 69 | "on": "יש", 70 | "off": "אין" 71 | } 72 | }, 73 | "ims_is_active_weather_warning_en": { 74 | "name": "Is Active Weather Warning", 75 | "state": { 76 | "on": "Alert", 77 | "off": "Clear" 78 | } 79 | } 80 | }, 81 | "sensor": { 82 | "ims_city_he": { 83 | "name": "ישוב" 84 | }, 85 | "ims_city_en": { 86 | "name": "City" 87 | }, 88 | "ims_forecast_time_he": { 89 | "name": "זמן התחזית" 90 | }, 91 | "ims_forecast_time_en": { 92 | "name": "Forecast Time" 93 | }, 94 | "ims_wind_direction_he": { 95 | "name": "כיוון רוח" 96 | }, 97 | "ims_wind_direction_en": { 98 | "name": "Wind Direction" 99 | }, 100 | "ims_wind_speed_he": { 101 | "name": "מהירות רוח" 102 | }, 103 | "ims_wind_speed_en": { 104 | "name": "Wind Speed" 105 | }, 106 | "ims_gust_speed_he": { 107 | "name": "מהירות משב רוח" 108 | }, 109 | "ims_gust_speed_en": { 110 | "name": "Gust Speed" 111 | }, 112 | "ims_pm10_he": { 113 | "name": "חומר חלקיקי (PM10)" 114 | }, 115 | "ims_pm10_en": { 116 | "name": "Coarse Particulate Matter (PM10)" 117 | }, 118 | "ims_dew_point_temp_he": { 119 | "name": "נקודת טל" 120 | }, 121 | "ims_dew_point_temp_en": { 122 | "name": "Dew Point Temperature" 123 | }, 124 | "ims_precipitation_he": { 125 | "name": "משקעים" 126 | }, 127 | "ims_precipitation_en": { 128 | "name": "Precipitation" 129 | }, 130 | "ims_precipitation_probability_he": { 131 | "name": "סיכוי לגשם" 132 | }, 133 | "ims_precipitation_probability_en": { 134 | "name": "Chance of Rain" 135 | }, 136 | "ims_temperature_he": { 137 | "name": "טמפרטורה" 138 | }, 139 | "ims_temperature_en": { 140 | "name": "Temperature" 141 | }, 142 | "ims_humidity_he": { 143 | "name": "לחות" 144 | }, 145 | "ims_humidity_en": { 146 | "name": "Humidity" 147 | }, 148 | "ims_feels_like_he": { 149 | "name": "מרגיש כמו" 150 | }, 151 | "ims_feels_like_en": { 152 | "name": "Feels Like" 153 | }, 154 | "ims_current_uv_index_he": { 155 | "name": "אינדקס קרינה נוכחי" 156 | }, 157 | "ims_current_uv_index_en": { 158 | "name": "Current UV Index" 159 | }, 160 | "ims_max_uv_index_he": { 161 | "name": "אינדקס קרינה מירבי" 162 | }, 163 | "ims_max_uv_index_en": { 164 | "name": "Maximum UV Index" 165 | }, 166 | "ims_current_uv_level_he": { 167 | "name": "רמת קרינה נוכחית", 168 | "state": { 169 | "extreme": "קיצוני", 170 | "very_high": "גבוה מאוד", 171 | "high": "גבוה", 172 | "moderate": "בינוני", 173 | "low": "נמוך" 174 | } 175 | }, 176 | "ims_current_uv_level_en": { 177 | "name": "Current UV Level", 178 | "state": { 179 | "extreme": "Extreme", 180 | "very_high": "Very High", 181 | "high": "High", 182 | "moderate": "Moderate", 183 | "low": "Low" 184 | } 185 | }, 186 | "ims_weather_warnings_en": { 187 | "name": "Weather Warnings", 188 | "state_attributes": { 189 | "warnings": { "name": "Warnings" } 190 | } 191 | }, 192 | "ims_weather_warnings_he": { 193 | "name": "אזהרות מז\"א", 194 | "state_attributes": { 195 | "warnings": { "name": "אזהרות" } 196 | } 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ims-custom-component 3 | 4 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration) 5 | [![GitHub Release][releases-shield]][releases] 6 | [![GitHub Activity][commits-shield]][commits] 7 | [![License][license-shield]](LICENSE) 8 | 9 | ![Project Maintenance][maintenance-shield] 10 | [![BuyMeCoffee][buymecoffeebadge]][buymecoffee] 11 | 12 | 13 | IMS custom component for HomeAssistant allows you to integrate the Israel Meteorological Service easily and with minimal configuration. 14 | With IMS, you can get the following information for the current status (Updates every hour) and future weather forecast: 15 | * Temperature 16 | * Feels Like 17 | * Humidity 18 | * Wind speed 19 | * Rain status 20 | * Weather Alerts 21 | 22 | [![IMS custom component](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/ims.png?raw=true "IMS custom component")](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/ims.png?raw=true "IMS custom component") 23 | 24 | 25 | 26 | And also, the forecast data for today and the next four days in six hours intervals includes the following information: 27 | * Maximum temperature. 28 | * Minimum temperature. 29 | * Max UVI. 30 | * Weather. 31 | * Daily forecast. 32 | * Weather forecast. 33 | * Temperature. 34 | 35 | 36 | [![IMS custom component](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/ims_attributes.png?raw=true "IMS custom component")](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/ims_attributes.png?raw=true "IMS custom component") 37 | 38 | 39 | ### Installation 40 | The IMS custom component can be installed from HACS default repository. 41 | 42 | **Restart** the Home Assistant instance to load ims integration before moving on 43 | 44 | Finally, use the UI to add the integration: 45 | 46 | Under settings, go to "Devices & Services" 47 | 48 | [![IMS custom component](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/settings-devices.png?raw=true "IMS custom component")](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/settings-devices.png?raw=true "IMS custom component") 49 | 50 | In the lower left cornet click on "Add Integration" button 51 | [![IMS custom component](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/add-integration.png?raw=true "IMS custom component")](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/add-integration.png?raw=true "IMS custom component") 52 | 53 | In the list of integrations, search for IMS: 54 | 55 | [![IMS custom component](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/select-brand.png?raw=true "IMS custom component")](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/select-brand.png?raw=true "IMS custom component") 56 | 57 | Enter the relevant parameters (Location and Language) and click sthe submit button in the bottom: 58 | 59 | [![IMS custom component](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/submit-settings.png?raw=true "IMS custom component")](https://github.com/GuyKh/ims-custom-component/blob/main/screenshots/submit-settings.png?raw=true "IMS custom component") 60 | 61 | 62 | The Languages can be one of two: 63 | * he - For hebrew 64 | * en - For english. 65 | 66 | The city code must be one of the codes in the following table: 67 | 68 | | Id | Location | 69 | | ------------ | ----------- | 70 | | 1| Jerusalem| 71 | | 2| Tel Aviv - Yafo| 72 | | 3| Haifa| 73 | | 4| Rishon le Zion| 74 | | 5| Petah Tiqva| 75 | | 6| Ashdod| 76 | | 7| Netania| 77 | | 8| Beer Sheva| 78 | | 9| Bnei Brak| 79 | | 10| Holon| 80 | | 11| Ramat Gan| 81 | | 12| Asheqelon| 82 | | 13| Rehovot| 83 | | 14| Bat Yam| 84 | | 15| Bet Shemesh| 85 | | 16| Kfar Sava| 86 | | 17| Herzliya| 87 | | 18| Hadera| 88 | | 19| Modiin| 89 | | 20| Ramla| 90 | | 21| Raanana| 91 | | 22| Modiin Illit| 92 | | 23| Rahat| 93 | | 24| Hod Hasharon| 94 | | 25| Givatayim| 95 | | 26| Kiryat Ata| 96 | | 27| Nahariya| 97 | | 28| Beitar Illit| 98 | | 29| Um al-Fahm| 99 | | 30| Kiryat Gat| 100 | | 31| Eilat| 101 | | 32| Rosh Haayin| 102 | | 33| Afula| 103 | | 34| Nes-Ziona| 104 | | 35| Akko| 105 | | 36| Elad| 106 | | 37| Ramat Hasharon| 107 | | 38| Karmiel| 108 | | 39| Yavneh| 109 | | 40| Tiberias| 110 | | 41| Tayibe| 111 | | 42| Kiryat Motzkin| 112 | | 43| Shfaram| 113 | | 44| Nof Hagalil| 114 | | 45| Kiryat Yam| 115 | | 46| Kiryat Bialik| 116 | | 47| Kiryat Ono| 117 | | 48| Maale Adumim| 118 | | 49| Or Yehuda| 119 | | 50| Zefat| 120 | | 51| Netivot| 121 | | 52| Dimona| 122 | | 53| Tamra | 123 | | 54| Sakhnin| 124 | | 55| Yehud| 125 | | 56| Baka al-Gharbiya| 126 | | 57| Ofakim| 127 | | 58| Givat Shmuel| 128 | | 59| Tira| 129 | | 60| Arad| 130 | | 61| Migdal Haemek| 131 | | 62| Sderot| 132 | | 63| Araba| 133 | | 64| Nesher| 134 | | 65| Kiryat Shmona| 135 | | 66| Yokneam Illit| 136 | | 67| Kafr Qassem| 137 | | 68| Kfar Yona| 138 | | 69| Qalansawa| 139 | | 70| Kiryat Malachi| 140 | | 71| Maalot-Tarshiha| 141 | | 72| Tirat Carmel| 142 | | 73| Ariel| 143 | | 74| Or Akiva| 144 | | 75| Bet Shean| 145 | | 76| Mizpe Ramon| 146 | | 77| Lod| 147 | | 78| Nazareth| 148 | | 79| Qazrin| 149 | | 80| En Gedi| 150 | | 200| Nimrod Fortress| 151 | | 201| Banias| 152 | | 202| Tel Dan| 153 | | 203| Snir Stream| 154 | | 204| Horshat Tal | 155 | | 205| Ayun Stream| 156 | | 206| Hula| 157 | | 207| Tel Hazor| 158 | | 208| Akhziv| 159 | | 209| Yehiam Fortress| 160 | | 210| Baram| 161 | | 211| Amud Stream| 162 | | 212| Korazim| 163 | | 213| Kfar Nahum| 164 | | 214| Majrase | 165 | | 215| Meshushim Stream| 166 | | 216| Yehudiya | 167 | | 217| Gamla| 168 | | 218| Kursi | 169 | | 219| Hamat Tiberias| 170 | | 220| Arbel| 171 | | 221| En Afek| 172 | | 222| Tzipori| 173 | | 223| Hai-Bar Carmel| 174 | | 224| Mount Carmel| 175 | | 225| Bet Shearim| 176 | | 226| Mishmar HaCarmel | 177 | | 227| Nahal Me‘arot| 178 | | 228| Dor-HaBonim| 179 | | 229| Tel Megiddo| 180 | | 230| Kokhav HaYarden| 181 | | 231| Maayan Harod| 182 | | 232| Bet Alpha| 183 | | 233| Gan HaShlosha| 184 | | 235| Taninim Stream| 185 | | 236| Caesarea| 186 | | 237| Tel Dor| 187 | | 238| Mikhmoret Sea Turtle| 188 | | 239| Beit Yanai| 189 | | 240| Apollonia| 190 | | 241| Mekorot HaYarkon| 191 | | 242| Palmahim| 192 | | 243| Castel| 193 | | 244| En Hemed| 194 | | 245| City of David| 195 | | 246| Me‘arat Soreq| 196 | | 248| Bet Guvrin| 197 | | 249| Sha’ar HaGai| 198 | | 250| Migdal Tsedek| 199 | | 251| Haniya Spring| 200 | | 252| Sebastia| 201 | | 253| Mount Gerizim| 202 | | 254| Nebi Samuel| 203 | | 255| En Prat| 204 | | 256| En Mabo‘a| 205 | | 257| Qasr al-Yahud| 206 | | 258| Good Samaritan| 207 | | 259| Euthymius Monastery| 208 | | 261| Qumran| 209 | | 262| Enot Tsukim| 210 | | 263| Herodium| 211 | | 264| Tel Hebron| 212 | | 267| Masada | 213 | | 268| Tel Arad| 214 | | 269| Tel Beer Sheva| 215 | | 270| Eshkol| 216 | | 271| Mamshit| 217 | | 272| Shivta| 218 | | 273| Ben-Gurion’s Tomb| 219 | | 274| En Avdat| 220 | | 275| Avdat| 221 | | 277| Hay-Bar Yotvata| 222 | | 278| Coral Beach| 223 | 224 | [buymecoffee]: https://www.buymeacoffee.com/guykh 225 | [buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge 226 | [commits-shield]: https://img.shields.io/github/commit-activity/y/guykh/ims-custom-component.svg?style=for-the-badge 227 | [commits]: https://github.com/guykh/ims-custom-component/commits/main 228 | [license-shield]: https://img.shields.io/github/license/guykh/ims-custom-component.svg?style=for-the-badge 229 | [maintenance-shield]: https://img.shields.io/badge/maintainer-Guy%20Khmelnitsky%20%40GuyKh-blue.svg?style=for-the-badge 230 | [releases-shield]: https://img.shields.io/github/release/guykh/ims-custom-component.svg?style=for-the-badge 231 | [releases]: https://github.com/guykh/ims-custom-component/releases 232 | 233 | -------------------------------------------------------------------------------- /custom_components/ims/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Location is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect" 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "city": "City", 13 | "language": "Language", 14 | "mode": "Forecast mode for the Weather entity", 15 | "name": "Integration Name", 16 | "images_path": "Path to download the images to", 17 | "update_interval": "Minutes to wait between updates. Reducing this below 15 minutes is not recommended.", 18 | "ims_platform": "Weather Entity and/or Sensor Entity. Sensor will create entities for each condition at each time. If unsure, only select Weather!", 19 | "monitored_conditions": "Monitored conditions to create sensors for. Only used if sensors are requested." 20 | }, 21 | "description": "Set up IMS Weather integration", 22 | "data_description": {} 23 | } 24 | } 25 | }, 26 | "options": { 27 | "step": { 28 | "init": { 29 | "data": { 30 | "city": "City", 31 | "language": "Language", 32 | "mode": "Forecast mode for the Weather entity", 33 | "name": "Integration Name", 34 | "units": "Units for sensors. Only used for if sensors are requested.", 35 | "images_path": "Path to download the images to", 36 | "update_interval": "Minutes to wait between updates. Reducing this below 15 minutes is not recommended.", 37 | "ims_platform": "Weather Entity and/or Sensor Entity. Sensor will create entities for each condition at each time. If unsure, only select Weather!", 38 | "monitored_conditions": "Monitored conditions to create sensors for. Only used if sensors are requested.\n NOTE: Removing sensors will produce orphaned entities that need to be deleted." 39 | }, 40 | "description": "Set up IMS Weather integration", 41 | "data_description": {} 42 | } 43 | } 44 | }, 45 | "services": { 46 | "debug_get_coordinator_data": { 47 | "name": "Get IMS Coordinator Data", 48 | "description": "Fetch and return the coordinator data for debugging purposes." 49 | } 50 | }, 51 | "entity": { 52 | "binary_sensor": { 53 | "ims_is_raining_he": { 54 | "name": "גשם", 55 | "state": { 56 | "on": "יורד", 57 | "off": "לא יורד" 58 | } 59 | }, 60 | "ims_is_raining_en": { 61 | "name": "Rain", 62 | "state": { 63 | "on": "Raining", 64 | "off": "Not Raining" 65 | } 66 | }, 67 | "ims_is_active_weather_warning_he": { 68 | "name": "יש התראת מז\"א בתוקף", 69 | "state": { 70 | "on": "יש", 71 | "off": "אין" 72 | } 73 | }, 74 | "ims_is_active_weather_warning_en": { 75 | "name": "Is Active Weather Warning", 76 | "state": { 77 | "on": "Alert", 78 | "off": "Clear" 79 | } 80 | } 81 | }, 82 | "sensor": { 83 | "ims_city_he": { 84 | "name": "ישוב" 85 | }, 86 | "ims_city_en": { 87 | "name": "City" 88 | }, 89 | "ims_forecast_time_he": { 90 | "name": "זמן התחזית" 91 | }, 92 | "ims_forecast_time_en": { 93 | "name": "Forecast Time" 94 | }, 95 | "ims_wind_direction_he": { 96 | "name": "כיוון רוח" 97 | }, 98 | "ims_wind_direction_en": { 99 | "name": "Wind Direction" 100 | }, 101 | "ims_wind_speed_he": { 102 | "name": "מהירות רוח" 103 | }, 104 | "ims_wind_speed_en": { 105 | "name": "Wind Speed" 106 | }, 107 | "ims_gust_speed_he": { 108 | "name": "מהירות משב רוח" 109 | }, 110 | "ims_gust_speed_en": { 111 | "name": "Gust Speed" 112 | }, 113 | "ims_precipitation_he": { 114 | "name": "משקעים" 115 | }, 116 | "ims_precipitation_en": { 117 | "name": "Precipitation" 118 | }, 119 | "ims_pm10_he": { 120 | "name": "חומר חלקיקי (PM10)" 121 | }, 122 | "ims_pm10_en": { 123 | "name": "Coarse Particulate Matter (PM10)" 124 | }, 125 | "ims_dew_point_temp_he": { 126 | "name": "נקודת טל" 127 | }, 128 | "ims_dew_point_temp_en": { 129 | "name": "Dew Point Temperature" 130 | }, 131 | "ims_precipitation_probability_he": { 132 | "name": "סיכוי לגשם" 133 | }, 134 | "ims_precipitation_probability_en": { 135 | "name": "Chance of Rain" 136 | }, 137 | "ims_temperature_he": { 138 | "name": "טמפרטורה" 139 | }, 140 | "ims_temperature_en": { 141 | "name": "Temperature" 142 | }, 143 | "ims_humidity_he": { 144 | "name": "לחות" 145 | }, 146 | "ims_humidity_en": { 147 | "name": "Humidity" 148 | }, 149 | "ims_feels_like_he": { 150 | "name": "מרגיש כמו" 151 | }, 152 | "ims_feels_like_en": { 153 | "name": "Feels Like" 154 | }, 155 | "ims_current_uv_index_he": { 156 | "name": "אינדקס קרינה נוכחי" 157 | }, 158 | "ims_current_uv_index_en": { 159 | "name": "Current UV Index" 160 | }, 161 | "ims_max_uv_index_he": { 162 | "name": "אינדקס קרינה מירבי" 163 | }, 164 | "ims_max_uv_index_en": { 165 | "name": "Maximum UV Index" 166 | }, 167 | "ims_current_uv_level_he": { 168 | "name": "רמת קרינה נוכחית", 169 | "state": { 170 | "extreme": "קיצוני", 171 | "very_high": "גבוה מאוד", 172 | "high": "גבוה", 173 | "moderate": "בינוני", 174 | "low": "נמוך" 175 | } 176 | }, 177 | "ims_current_uv_level_en": { 178 | "name": "Current UV Level", 179 | "state": { 180 | "extreme": "Extreme", 181 | "very_high": "Very High", 182 | "high": "High", 183 | "moderate": "Moderate", 184 | "low": "Low" 185 | } 186 | }, 187 | "ims_weather_warnings_en": { 188 | "name": "Weather Warnings", 189 | "state_attributes": { 190 | "warnings": { "name": "Warnings" } 191 | } 192 | }, 193 | "ims_weather_warnings_he": { 194 | "name": "אזהרות מז\"א", 195 | "state_attributes": { 196 | "warnings": { "name": "אזהרות" } 197 | } 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /custom_components/ims/const.py: -------------------------------------------------------------------------------- 1 | """Consts for the OpenWeatherMap.""" 2 | 3 | from __future__ import annotations 4 | import types 5 | 6 | import homeassistant.util.dt as dt_util 7 | 8 | from homeassistant.components.weather import ( 9 | ATTR_CONDITION_CLEAR_NIGHT, 10 | ATTR_CONDITION_CLOUDY, 11 | ATTR_CONDITION_EXCEPTIONAL, 12 | ATTR_CONDITION_FOG, 13 | ATTR_CONDITION_HAIL, 14 | ATTR_CONDITION_LIGHTNING_RAINY, 15 | ATTR_CONDITION_PARTLYCLOUDY, 16 | ATTR_CONDITION_POURING, 17 | ATTR_CONDITION_RAINY, 18 | ATTR_CONDITION_SNOWY, 19 | ATTR_CONDITION_SNOWY_RAINY, 20 | ATTR_CONDITION_SUNNY, 21 | ATTR_CONDITION_WINDY, 22 | ) 23 | from homeassistant.const import ( 24 | Platform, 25 | ) 26 | 27 | DATETIME_FORMAT = "%H:%M:%S %d/%m/%Y" 28 | DOMAIN = "ims" 29 | DEFAULT_NAME = "IMS Weather" 30 | IMS_TIMEZONE = dt_util.get_time_zone("Asia/Jerusalem") 31 | DEFAULT_LANGUAGE = "en" 32 | DEFAULT_UPDATE_INTERVAL = 60 33 | DEFAULT_IMAGE_PATH = "/tmp" 34 | ATTRIBUTION = "Data provided by Israel Meteorological Service" 35 | MANUFACTURER = "IMS" 36 | CONF_UPDATE_INTERVAL = "update_interval" 37 | CONF_CITY = "city" 38 | CONF_LANGUAGE = "language" 39 | CONF_MODE = "mode" 40 | CONF_IMAGES_PATH = "images_path" 41 | CONFIG_FLOW_VERSION = 2 42 | ENTRY_NAME = "name" 43 | ENTRY_WEATHER_COORDINATOR = "weather_coordinator" 44 | ATTR_API_DEW_POINT = "due_point_Temp" 45 | ATTR_API_FEELS_LIKE_TEMPERATURE = "feels_like" 46 | ATTR_API_FORECAST_DATE = "forecast_date" 47 | ATTR_API_FORECAST_TIME = "forecast_time" 48 | ATTR_API_HEAT_STRESS = "heat_stress" 49 | ATTR_API_HEAT_STRESS_LEVEL = "heat_stress_level" 50 | ATTR_API_MAXIMUM_TEMPERATURE = "maximum_temperature" 51 | ATTR_API_MAXIMUM_UV_INDEX = "maximum_uvi" 52 | ATTR_API_MINIMUM_TEMPERATURE = "minimum_temperature" 53 | ATTR_API_PRECIPITATION = "precipitation" 54 | ATTR_API_RAIN = "rain" 55 | ATTR_API_RELATIVE_HUMIDITY = "relative_humidity" 56 | ATTR_API_TEMPERATURE = "temperature" 57 | ATTR_API_UV_INDEX = "u_v_index" 58 | ATTR_API_UV_LEVEL = "u_v_level" 59 | ATTR_API_WEATHER_CODE = "weather_code" 60 | ATTR_API_WIND_BEARING = "wind_direction_id" 61 | ATTR_API_WIND_CHILL = "wind_chill" 62 | ATTR_API_WIND_SPEED = "wind_speed" 63 | UPDATE_LISTENER = "update_listener" 64 | PLATFORMS = [Platform.SENSOR, Platform.WEATHER, Platform.BINARY_SENSOR] 65 | IMS_PLATFORMS = ["Sensor", "Weather"] 66 | IMS_PLATFORM = "ims_platform" 67 | IMS_PREVPLATFORM = "ims_prevplatform" 68 | 69 | FORECAST_MODE_HOURLY = "hourly" 70 | FORECAST_MODE_DAILY = "daily" 71 | DEFAULT_FORECAST_MODE = FORECAST_MODE_DAILY 72 | FORECAST_MODES = [FORECAST_MODE_HOURLY, FORECAST_MODE_DAILY] 73 | 74 | TYPE_CITY = "city" 75 | TYPE_CURRENT_UV_INDEX = "current_uv_index" 76 | TYPE_CURRENT_UV_LEVEL = "current_uv_level" 77 | TYPE_DEW_POINT_TEMP = "dew_point_temp" 78 | TYPE_WEATHER_WARNINGS = "weather_warnings" 79 | TYPE_FEELS_LIKE = "feels_like" 80 | TYPE_FORECAST_PREFIX = "forecast_" 81 | TYPE_FORECAST_TODAY = "today" 82 | TYPE_FORECAST_DAY1 = "day1" 83 | TYPE_FORECAST_DAY2 = "day2" 84 | TYPE_FORECAST_DAY3 = "day3" 85 | TYPE_FORECAST_DAY4 = "day4" 86 | TYPE_FORECAST_DAY5 = "day5" 87 | TYPE_FORECAST_DAY6 = "day6" 88 | TYPE_FORECAST_DAY7 = "day7" 89 | TYPE_FORECAST_TIME = "forecast_time" 90 | TYPE_GUST_SPEED = "gust_speed" 91 | TYPE_HEAT_STRESS = "heat_stress" 92 | TYPE_HEAT_STRESS_LEVEL = "heat_stress_level" 93 | TYPE_HUMIDITY = "humidity" 94 | TYPE_IS_RAINING = "is_raining" 95 | TYPE_IS_ACTIVE_WEATHER_WARNING = "is_active_weather_warning" 96 | TYPE_MAX_TEMP = "max_temp" 97 | TYPE_MAX_UV_INDEX = "max_uv_index" 98 | TYPE_MIN_TEMP = "min_temp" 99 | TYPE_PRECIPITATION = "precipitation" 100 | TYPE_PRECIPITATION_PROBABILITY = "precipitation_probability" 101 | TYPE_PM10 = "pm10" 102 | TYPE_TEMPERATURE = "temperature" 103 | TYPE_WAVE_HEIGHT = "wave_height" 104 | TYPE_WEATHER_CODE = "weather_code" 105 | TYPE_WIND_CHILL = "wind_chill" 106 | TYPE_WIND_DIRECTION = "wind_direction" 107 | TYPE_WIND_SPEED = "wind_speed" 108 | 109 | FIELD_NAME_DEW_POINT_TEMP = "due_point_temp" 110 | FIELD_NAME_FEELS_LIKE = "feels_like" 111 | FIELD_NAME_FORECAST_TIME = "forecast_time" 112 | FIELD_NAME_GUST_SPEED = "gust_speed" 113 | FIELD_NAME_HEAT_STRESS = "heat_stress" 114 | FIELD_NAME_HEAT_STRESS_LEVEL = "heat_stress_level" 115 | FIELD_NAME_HUMIDITY = "relative_humidity" 116 | FIELD_NAME_WARNING = "warning" 117 | FIELD_NAME_LOCATION = "location" 118 | FIELD_NAME_MAX_TEMP = "max_temp" 119 | FIELD_NAME_MIN_TEMP = "min_temp" 120 | FIELD_NAME_PM10 = "pm10" 121 | FIELD_NAME_RAIN = "rain" 122 | FIELD_NAME_RAIN_CHANCE = "rain_chance" 123 | FIELD_NAME_TEMPERATURE = "temperature" 124 | FIELD_NAME_UV_INDEX = "u_v_index" 125 | FIELD_NAME_UV_INDEX_FACTOR = "u_v_i_factor" 126 | FIELD_NAME_UV_INDEX_MAX = "u_v_i_max" 127 | FIELD_NAME_UV_LEVEL = "u_v_level" 128 | FIELD_NAME_WAVE_HEIGHT = "wave_height" 129 | FIELD_NAME_WEATHER_CODE = "weather_code" 130 | FIELD_NAME_WIND_CHILL = "wind_chill" 131 | FIELD_NAME_WIND_DIRECTION_ID = "wind_direction_id" 132 | FIELD_NAME_WIND_SPEED = "wind_speed" 133 | 134 | LANGUAGES = ["en", "he"] 135 | 136 | IMS_SENSOR_KEY_PREFIX = "ims_" 137 | 138 | 139 | FORECAST_MODE = types.SimpleNamespace() 140 | FORECAST_MODE.CURRENT = "current" 141 | FORECAST_MODE.DAILY = "daily" 142 | FORECAST_MODE.HOURLY = "hourly" 143 | 144 | 145 | UV_LEVEL_EXTREME = "extreme" 146 | UV_LEVEL_VHIGH = "very_high" 147 | UV_LEVEL_HIGH = "high" 148 | UV_LEVEL_MODERATE = "moderate" 149 | UV_LEVEL_LOW = "low" 150 | 151 | # Based on https://ims.gov.il/en/wind_directions 152 | WIND_DIRECTIONS = { 153 | None: None, 154 | 0: None, 155 | 1: float(360), 156 | 2: float(23), 157 | 3: float(45), 158 | 4: float(68), 159 | 5: float(90), 160 | 6: float(113), 161 | 7: float(135), 162 | 8: float(150), 163 | 9: float(180), 164 | 10: float(203), 165 | 11: float(225), 166 | 12: float(248), 167 | 13: float(270), 168 | 14: float(293), 169 | 15: float(315), 170 | 16: float(338), 171 | 17: float(0), 172 | } 173 | 174 | # Based on https://ims.gov.il/en/weather_codes 175 | WEATHER_CODE_TO_CONDITION = { 176 | None: None, 177 | "None": None, 178 | "0": None, 179 | "1010": ATTR_CONDITION_EXCEPTIONAL, 180 | "1020": ATTR_CONDITION_LIGHTNING_RAINY, 181 | "1060": ATTR_CONDITION_SNOWY, 182 | "1070": ATTR_CONDITION_SNOWY, 183 | "1080": ATTR_CONDITION_SNOWY_RAINY, 184 | "1140": ATTR_CONDITION_POURING, 185 | "1160": ATTR_CONDITION_FOG, 186 | "1220": ATTR_CONDITION_PARTLYCLOUDY, 187 | "1220-night": ATTR_CONDITION_PARTLYCLOUDY, # no "-night" 188 | "1230": ATTR_CONDITION_CLOUDY, 189 | "1250": ATTR_CONDITION_SUNNY, 190 | "1250-night": ATTR_CONDITION_CLEAR_NIGHT, 191 | "1260": ATTR_CONDITION_WINDY, 192 | "1270": ATTR_CONDITION_SUNNY, 193 | "1300": ATTR_CONDITION_HAIL, 194 | "1310": ATTR_CONDITION_SUNNY, 195 | "1320": ATTR_CONDITION_HAIL, 196 | "1510": ATTR_CONDITION_LIGHTNING_RAINY, 197 | "1520": ATTR_CONDITION_SNOWY, 198 | "1530": ATTR_CONDITION_RAINY, 199 | "1540": ATTR_CONDITION_RAINY, 200 | "1560": ATTR_CONDITION_RAINY, 201 | "1570": ATTR_CONDITION_EXCEPTIONAL, 202 | "1580": ATTR_CONDITION_EXCEPTIONAL, 203 | "1590": ATTR_CONDITION_EXCEPTIONAL, 204 | } 205 | 206 | 207 | WEATHER_CODE_TO_ICON = { 208 | "1010": "mdi:weather-dust", 209 | "1020": "mdi:weather-lightning-rainy", 210 | "1060": "mdi:weather-snowy", 211 | "1070": "mdi:weather-snowy", 212 | "1080": "mdi:weather-snowy-rainy", 213 | "1140": "mdi:weather-pouring", 214 | "1160": "mdi:weather-fog", 215 | "1220": "mdi:weather-partly-cloudy", 216 | "1220-night": "mdi:weather-partly-cloudy-night", 217 | "1230": "mdi:weather-cloudy", 218 | "1250": "mdi:weather-sunny", 219 | "1250-night": "mdi:clear-night", 220 | "1260": "mdi:weather-windy", 221 | "1270": "mdi:weather-fog", 222 | "1300": "mdi:snowflake-melt", 223 | "1310": "mdi:weather-sunny-alert", 224 | "1320": "mdi:snowflake-alert", 225 | "1510": "mdi:weather-lightning", 226 | "1520": "mdi:weather-snowy-heavy", 227 | "1530": "mdi:weather-partly-rainy", 228 | "1540": "mdi:weather-rainy", 229 | "1560": "mdi:weather-rainy", 230 | "1570": "mdi:weather-dust", 231 | "1580": "mdi:weather-sunny-alert", 232 | "1590": "mdi:snowflake-alert", 233 | } 234 | -------------------------------------------------------------------------------- /custom_components/ims/translations/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Localização já está configurada" 5 | }, 6 | "error": { 7 | "cannot_connect": "Falha ao conectar" 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "city": "Cidade", 13 | "language": "Idioma", 14 | "mode": "Modo de previsão para a entidade Meteorológica", 15 | "name": "Nome da Integração", 16 | "images_path": "Caminho para download das imagens", 17 | "update_interval": "Minutos de espera entre atualizações. Reduzir este valor abaixo de 15 minutos não é recomendado.", 18 | "ims_platform": "Entidade Meteorológica e/ou Entidade de Sensor. O sensor criará entidades para cada condição a cada hora. Se estiver em dúvida, selecione apenas Meteorologia!", 19 | "monitored_conditions": "Condições monitorizadas para as quais serão criados sensores. Só é usado se os sensores forem solicitados." 20 | }, 21 | "description": "Configurar a integração IMS Weather", 22 | "data_description": {} 23 | } 24 | } 25 | }, 26 | "options": { 27 | "step": { 28 | "init": { 29 | "data": { 30 | "city": "Cidade", 31 | "language": "Idioma", 32 | "mode": "Modo de previsão para a entidade Meteorológica", 33 | "name": "Nome da Integração", 34 | "units": "Unidades para os sensores. Só é usado se os sensores forem solicitados.", 35 | "images_path": "Caminho para download das imagens", 36 | "update_interval": "Minutos de espera entre atualizações. Reduzir este valor abaixo de 15 minutos não é recomendado.", 37 | "ims_platform": "Entidade Meteorológica e/ou Entidade de Sensor. O sensor criará entidades para cada condição a cada hora. Se estiver em dúvida, selecione apenas Meteorologia!", 38 | "monitored_conditions": "Condições monitorizadas para as quais serão criados sensores. Só é usado se os sensores forem solicitados.\n NOTA: Remover sensores criará entidades órfãs que precisam de ser apagadas." 39 | }, 40 | "description": "Configurar a integração IMS Weather", 41 | "data_description": {} 42 | } 43 | } 44 | }, 45 | "services": { 46 | "debug_get_coordinator_data": { 47 | "name": "Get IMS Coordinator Data", 48 | "description": "Fetch and return the coordinator data for debugging purposes." 49 | } 50 | }, 51 | "entity": { 52 | "binary_sensor": { 53 | "ims_is_raining_he": { 54 | "name": "Chuva", 55 | "state": { 56 | "on": "A chover", 57 | "off": "Não está a chover" 58 | } 59 | }, 60 | "ims_is_raining_en": { 61 | "name": "Chuva", 62 | "state": { 63 | "on": "A chover", 64 | "off": "Não está a chover" 65 | } 66 | }, 67 | "ims_is_active_weather_warning_he": { 68 | "name": "Existe um alerta meteorológico ativo", 69 | "state": { 70 | "on": "Existe", 71 | "off": "Não Existe" 72 | } 73 | }, 74 | "ims_is_active_weather_warning_en": { 75 | "name": "Existe um alerta meteorológico ativo", 76 | "state": { 77 | "on": "Existe", 78 | "off": "Não Existe" 79 | } 80 | }, 81 | "ims_weather_warnings_en": { 82 | "name": "Alertas meteorológicos", 83 | "state_attributes": { 84 | "warnings": { "name": "Alertas" } 85 | } 86 | }, 87 | "ims_weather_warnings_he": { 88 | "name": "Alertas meteorológicos", 89 | "state_attributes": { 90 | "warnings": { "name": "Alertas" } 91 | } 92 | } 93 | }, 94 | "sensor": { 95 | "ims_city_he": { 96 | "name": "Localidade" 97 | }, 98 | "ims_city_en": { 99 | "name": "Cidade" 100 | }, 101 | "ims_forecast_time_he": { 102 | "name": "Hora de previsão" 103 | }, 104 | "ims_forecast_time_en": { 105 | "name": "Hora de previsão" 106 | }, 107 | "ims_wind_direction_he": { 108 | "name": "Direção do Vento" 109 | }, 110 | "ims_wind_direction_en": { 111 | "name": "Direção do Vento" 112 | }, 113 | "ims_wind_speed_he": { 114 | "name": "Velocidade do Vento" 115 | }, 116 | "ims_wind_speed_en": { 117 | "name": "Velocidade do Vento" 118 | }, 119 | "ims_gust_speed_he": { 120 | "name": "Velocidade da Rajada" 121 | }, 122 | "ims_gust_speed_en": { 123 | "name": "Velocidade da Rajada" 124 | }, 125 | "ims_precipitation_he": { 126 | "name": "Precipitação" 127 | }, 128 | "ims_precipitation_en": { 129 | "name": "Precipitação" 130 | }, 131 | "ims_dew_point_temp_he": { 132 | "name": "Ponto de Orvalho" 133 | }, 134 | "ims_dew_point_temp_en": { 135 | "name": "Ponto de Orvalho" 136 | }, 137 | "ims_pm10_he": { 138 | "name": "Matéria Particulada Grossa (PM10)" 139 | }, 140 | "ims_pm10_en": { 141 | "name": "Matéria Particulada Grossa (PM10)" 142 | }, 143 | "ims_precipitation_probability_he": { 144 | "name": "Probabilidade de Chuva" 145 | }, 146 | "ims_precipitation_probability_en": { 147 | "name": "Probabilidade de Chuva" 148 | }, 149 | "ims_temperature_he": { 150 | "name": "Temperatura" 151 | }, 152 | "ims_temperature_en": { 153 | "name": "Temperatura" 154 | }, 155 | "ims_humidity_he": { 156 | "name": "Humidade" 157 | }, 158 | "ims_humidity_en": { 159 | "name": "Humidade" 160 | }, 161 | "ims_feels_like_he": { 162 | "name": "Sensação Térmica" 163 | }, 164 | "ims_feels_like_en": { 165 | "name": "Sensação Térmica" 166 | }, 167 | "ims_current_uv_index_he": { 168 | "name": "Índice UV Atual" 169 | }, 170 | "ims_current_uv_index_en": { 171 | "name": "Índice UV Atual" 172 | }, 173 | "ims_max_uv_index_he": { 174 | "name": "Índice UV Máximo" 175 | }, 176 | "ims_max_uv_index_en": { 177 | "name": "Índice UV Máximo" 178 | }, 179 | "ims_current_uv_level_he": { 180 | "name": "Nível UV Atual", 181 | "state": { 182 | "extreme": "Extremo", 183 | "very_high": "Muito Alto", 184 | "high": "Alto", 185 | "moderate": "Moderado", 186 | "low": "Baixo" 187 | } 188 | }, 189 | "ims_current_uv_level_en": { 190 | "name": "Nível UV Atual", 191 | "state": { 192 | "extreme": "Extremo", 193 | "very_high": "Muito Alto", 194 | "high": "Alto", 195 | "moderate": "Moderado", 196 | "low": "Baixo" 197 | } 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /custom_components/ims/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from dataclasses import dataclass 3 | from datetime import timedelta 4 | from typing import Any 5 | 6 | from homeassistant.components.sensor import ( 7 | SensorEntityDescription, 8 | ) 9 | from homeassistant.config_entries import ConfigEntry 10 | from homeassistant.const import ( 11 | CONF_MODE, 12 | CONF_NAME, 13 | CONF_MONITORED_CONDITIONS, 14 | ) 15 | from homeassistant.core import HomeAssistant, callback 16 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 17 | 18 | from .const import ( 19 | CONF_CITY, 20 | CONF_LANGUAGE, 21 | CONF_IMAGES_PATH, 22 | CONF_UPDATE_INTERVAL, 23 | DOMAIN, 24 | ENTRY_NAME, 25 | ENTRY_WEATHER_COORDINATOR, 26 | UPDATE_LISTENER, 27 | PLATFORMS, 28 | IMS_PLATFORMS, 29 | IMS_PLATFORM, 30 | DEFAULT_LANGUAGE, 31 | FORECAST_MODE_HOURLY, 32 | ) 33 | 34 | from .weather_update_coordinator import WeatherUpdateCoordinator 35 | 36 | CONF_FORECAST = "forecast" 37 | CONF_HOURLY_FORECAST = "hourly_forecast" 38 | 39 | _LOGGER = logging.getLogger(__name__) 40 | ATTRIBUTION = "Powered by IMS Weather" 41 | 42 | 43 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 44 | """Set up IMS Weather as config entry.""" 45 | name = entry.data[CONF_NAME] 46 | city = _get_config_value(entry, CONF_CITY) 47 | forecast_mode = _get_config_value(entry, CONF_MODE, FORECAST_MODE_HOURLY) 48 | images_path = _get_config_value(entry, CONF_IMAGES_PATH) 49 | language = _get_config_value(entry, CONF_LANGUAGE, DEFAULT_LANGUAGE) 50 | ims_entity_platform = _get_config_value(entry, IMS_PLATFORM) 51 | ims_scan_int = entry.data[CONF_UPDATE_INTERVAL] 52 | conditions = _get_config_value(entry, CONF_MONITORED_CONDITIONS) 53 | 54 | # Extract list of int from forecast days/ hours string if present 55 | # _LOGGER.warning('forecast_days_type: ' + str(type(forecast_days))) 56 | is_legacy_city = False 57 | if isinstance(city, int | str): 58 | is_legacy_city = True 59 | 60 | city_id = city if is_legacy_city else city["lid"] 61 | 62 | unique_location = f"ims-{language}-{city_id}" 63 | 64 | hass.data.setdefault(DOMAIN, {}) 65 | # If coordinator already exists for this API key, we'll use that, otherwise 66 | # we have to create a new one 67 | if unique_location in hass.data[DOMAIN]: 68 | weather_coordinator = hass.data[DOMAIN].get(unique_location) 69 | _LOGGER.info( 70 | "An existing IMS weather coordinator already exists for this location. Using that one instead" 71 | ) 72 | else: 73 | weather_coordinator = WeatherUpdateCoordinator( 74 | city_id, language, timedelta(minutes=ims_scan_int), hass 75 | ) 76 | hass.data[DOMAIN][unique_location] = weather_coordinator 77 | # _LOGGER.warning('New Coordinator') 78 | 79 | # await weather_coordinator.async_refresh() 80 | await weather_coordinator.async_config_entry_first_refresh() 81 | 82 | hass.data[DOMAIN][entry.entry_id] = { 83 | ENTRY_NAME: name, 84 | ENTRY_WEATHER_COORDINATOR: weather_coordinator, 85 | CONF_CITY: city, 86 | CONF_LANGUAGE: language, 87 | CONF_MODE: forecast_mode, 88 | CONF_IMAGES_PATH: images_path, 89 | CONF_UPDATE_INTERVAL: ims_scan_int, 90 | IMS_PLATFORM: ims_entity_platform, 91 | CONF_MONITORED_CONDITIONS: conditions, 92 | } 93 | 94 | # If both platforms 95 | if (IMS_PLATFORMS[0] in ims_entity_platform) and ( 96 | IMS_PLATFORMS[1] in ims_entity_platform 97 | ): 98 | platforms = PLATFORMS 99 | # If only sensor 100 | elif IMS_PLATFORMS[0] in ims_entity_platform: 101 | platforms = [PLATFORMS[0], PLATFORMS[2]] 102 | # If only weather 103 | elif IMS_PLATFORMS[1] in ims_entity_platform: 104 | platforms = [PLATFORMS[1]] 105 | 106 | await hass.config_entries.async_forward_entry_setups(entry, platforms) 107 | 108 | update_listener = entry.add_update_listener(async_update_options) 109 | hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener 110 | 111 | # Register the debug service 112 | async def handle_debug_get_coordinator_data(call) -> None: # noqa: ANN001 ARG001 113 | # Log or return coordinator data 114 | data = weather_coordinator.data 115 | _LOGGER.info("Coordinator data: %s", data) 116 | hass.bus.async_fire("custom_component_debug_event", {"data": data}) 117 | 118 | hass.services.async_register( 119 | DOMAIN, "debug_get_coordinator_data", handle_debug_get_coordinator_data 120 | ) 121 | return True 122 | 123 | 124 | async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: 125 | """Update options.""" 126 | await hass.config_entries.async_reload(entry.entry_id) 127 | 128 | 129 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 130 | """Unload a config entry.""" 131 | ims_entity_prevplatform = hass.data[DOMAIN][entry.entry_id][IMS_PLATFORM] 132 | 133 | unload_ok = False 134 | # If both 135 | if (IMS_PLATFORMS[0] in ims_entity_prevplatform) and ( 136 | IMS_PLATFORMS[1] in ims_entity_prevplatform 137 | ): 138 | unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 139 | # If only sensor 140 | elif IMS_PLATFORMS[0] in ims_entity_prevplatform: 141 | unload_ok = await hass.config_entries.async_unload_platforms( 142 | entry, [PLATFORMS[0], PLATFORMS[2]] 143 | ) 144 | # If only Weather 145 | elif IMS_PLATFORMS[1] in ims_entity_prevplatform: 146 | unload_ok = await hass.config_entries.async_unload_platforms( 147 | entry, [PLATFORMS[1]] 148 | ) 149 | 150 | _LOGGER.info(f"Unloading IMS Weather. Successful: {unload_ok}") 151 | 152 | if unload_ok: 153 | update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] 154 | update_listener() 155 | 156 | hass.data[DOMAIN].pop(entry.entry_id) 157 | 158 | return True 159 | 160 | 161 | def _get_config_value(config_entry: ConfigEntry, key: str, default=None) -> Any: 162 | if config_entry.options: 163 | val = config_entry.options.get(key) 164 | if val: 165 | return val 166 | else: 167 | _LOGGER.warning("Key %s is missing from config_entry.options", key) 168 | return default 169 | val = config_entry.data.get(key) 170 | if val: 171 | return val 172 | else: 173 | _LOGGER.warning("Key %s is missing from config_entry.data", key) 174 | return default 175 | 176 | 177 | def _filter_domain_configs(elements, domain): 178 | return list(filter(lambda elem: elem["platform"] == domain, elements)) 179 | 180 | 181 | @dataclass(kw_only=True, frozen=True) 182 | class ImsSensorEntityDescription(SensorEntityDescription): 183 | """Describes IMS Weather sensor entity.""" 184 | 185 | field_name: str | None = None 186 | forecast_mode: str | None = None 187 | 188 | 189 | class ImsEntity(CoordinatorEntity): 190 | """Define a generic Ims entity.""" 191 | 192 | _attr_has_entity_name = True 193 | 194 | def __init__( 195 | self, 196 | coordinator: WeatherUpdateCoordinator, 197 | description: ImsSensorEntityDescription, 198 | ) -> None: 199 | """Initialize.""" 200 | super().__init__(coordinator) 201 | 202 | self._attr_extra_state_attributes = {} 203 | self._attr_unique_id = ( 204 | f"{description.key}_{coordinator.city}_{coordinator.language}" 205 | ) 206 | 207 | self.entity_id = "sensor." + description.key 208 | self._attr_translation_key = f"{description.key}_{coordinator.language}" 209 | self.entity_description = description 210 | 211 | @callback 212 | def _handle_coordinator_update(self) -> None: 213 | """Respond to a DataUpdateCoordinator update.""" 214 | self._update_from_latest_data() 215 | self.async_write_ha_state() 216 | 217 | @callback 218 | def _update_from_latest_data(self) -> None: 219 | """Update the entity from the latest data.""" 220 | raise NotImplementedError 221 | 222 | async def async_added_to_hass(self) -> None: 223 | """Handle entity which will be added.""" 224 | await super().async_added_to_hass() 225 | self._update_from_latest_data() 226 | -------------------------------------------------------------------------------- /custom_components/ims/weather.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import logging 3 | 4 | 5 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 6 | from homeassistant.helpers.typing import DiscoveryInfoType 7 | from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry 8 | from homeassistant.core import HomeAssistant, callback 9 | 10 | 11 | from homeassistant.components.weather import ( 12 | # PLATFORM_SCHEMA, 13 | Forecast, 14 | WeatherEntity, 15 | WeatherEntityFeature, 16 | ) 17 | 18 | from homeassistant.const import CONF_NAME, UnitOfSpeed, UnitOfPressure, UnitOfLength 19 | 20 | from .const import ( 21 | ATTRIBUTION, 22 | CONF_CITY, 23 | CONF_MODE, 24 | DOMAIN, 25 | FORECAST_MODE_HOURLY, 26 | IMS_PLATFORMS, 27 | IMS_PLATFORM, 28 | ENTRY_WEATHER_COORDINATOR, 29 | WEATHER_CODE_TO_CONDITION, 30 | WIND_DIRECTIONS, 31 | ) 32 | 33 | from homeassistant.const import UnitOfTemperature 34 | 35 | from .utils import get_hourly_weather_icon 36 | from .weather_update_coordinator import WeatherUpdateCoordinator 37 | 38 | _LOGGER = logging.getLogger(__name__) 39 | 40 | weather = None 41 | 42 | 43 | async def async_setup_platform( 44 | hass: HomeAssistant, 45 | config_entry: ConfigEntry, 46 | async_add_entities: AddEntitiesCallback, 47 | discovery_info: DiscoveryInfoType | None = None, 48 | ) -> None: 49 | """Import the platform into a config entry.""" 50 | _LOGGER.warning( 51 | "Configuration of IMS Weather (weather entity) in YAML is deprecated " 52 | "Your existing configuration has been imported into the UI automatically " 53 | "and can be safely removed from your configuration.yaml file" 54 | ) 55 | 56 | # Add source to config 57 | config_entry[IMS_PLATFORM] = [IMS_PLATFORMS[1]] 58 | 59 | hass.async_create_task( 60 | hass.config_entries.flow.async_init( 61 | DOMAIN, context={"source": SOURCE_IMPORT}, data=config_entry 62 | ) 63 | ) 64 | 65 | 66 | async def async_setup_entry( 67 | hass: HomeAssistant, 68 | config_entry: ConfigEntry, 69 | async_add_entities: AddEntitiesCallback, 70 | ) -> None: 71 | """Set up IMS Weather entity based on a config entry.""" 72 | domain_data = hass.data[DOMAIN][config_entry.entry_id] 73 | name = domain_data[CONF_NAME] 74 | weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] 75 | city = domain_data[CONF_CITY] 76 | forecast_mode = domain_data[CONF_MODE] 77 | 78 | is_legacy_city = False 79 | if isinstance(city, int | str): 80 | is_legacy_city = True 81 | 82 | unique_id = f"{config_entry.unique_id}" 83 | 84 | # Round Output 85 | output_round = "No" 86 | 87 | ims_weather = IMSWeather( 88 | name, 89 | unique_id, 90 | forecast_mode, 91 | weather_coordinator, 92 | city if is_legacy_city else city["name"], 93 | output_round, 94 | ) 95 | 96 | async_add_entities([ims_weather], False) 97 | 98 | 99 | def round_if_needed(value: int | float, output_round: bool): 100 | if output_round: 101 | return round(value, 0) + 0 102 | else: 103 | return round(value, 2) 104 | 105 | 106 | class IMSWeather(WeatherEntity): 107 | """Implementation of an IMSWeather sensor.""" 108 | 109 | _attr_attribution = ATTRIBUTION 110 | _attr_should_poll = False 111 | _attr_native_precipitation_unit = UnitOfLength.MILLIMETERS 112 | _attr_native_pressure_unit = UnitOfPressure.MBAR 113 | _attr_native_temperature_unit = UnitOfTemperature.CELSIUS 114 | _attr_native_visibility_unit = UnitOfLength.KILOMETERS 115 | _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR 116 | _attr_supported_features = ( 117 | WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY 118 | ) 119 | 120 | def __init__( 121 | self, 122 | name: str, 123 | unique_id, 124 | forecast_mode: str, 125 | weather_coordinator: WeatherUpdateCoordinator, 126 | city: str, 127 | output_round: str, 128 | ) -> None: 129 | """Initialize the sensor.""" 130 | self._attr_name = name 131 | self._weather_coordinator = weather_coordinator 132 | self._name = name 133 | self._mode = forecast_mode 134 | self._unique_id = unique_id 135 | self._city = city 136 | self._output_round = output_round == "Yes" 137 | self._ds_data = self._weather_coordinator.data 138 | 139 | @callback 140 | def _handle_coordinator_update(self) -> None: 141 | """Handle updated data from the coordinator.""" 142 | super()._handle_coordinator_update() 143 | assert self.platform.config_entry 144 | self.platform.config_entry.async_create_task( 145 | self.hass, self.async_update_listeners(("daily", "hourly")) 146 | ) 147 | 148 | @property 149 | def unique_id(self): 150 | """Return a unique_id for this entity.""" 151 | return self._unique_id 152 | 153 | @property 154 | def available(self): 155 | """Return if weather data is available from IMSWeather.""" 156 | return ( 157 | self._weather_coordinator.data is not None 158 | and self._weather_coordinator.data.current_weather is not None 159 | and self._weather_coordinator.data.forecast is not None 160 | ) 161 | 162 | @property 163 | def attribution(self): 164 | """Return the attribution.""" 165 | return self._weather_coordinator.data.current_weather.description 166 | 167 | @property 168 | def name(self): 169 | """Return the name of the sensor.""" 170 | return self._name 171 | 172 | @property 173 | def native_temperature(self): 174 | """Return the temperature.""" 175 | temperature = float(self._weather_coordinator.data.current_weather.temperature) 176 | 177 | return round_if_needed(temperature, self._output_round) 178 | 179 | @property 180 | def native_apparent_temperature(self): 181 | """Return the native apparent temperature (feel-like).""" 182 | feels_like_temperature = float( 183 | self._weather_coordinator.data.current_weather.feels_like 184 | ) 185 | 186 | return round_if_needed(feels_like_temperature, self._output_round) 187 | 188 | @property 189 | def humidity(self): 190 | """Return the humidity.""" 191 | humidity = float(self._weather_coordinator.data.current_weather.humidity) 192 | 193 | return round_if_needed(humidity, self._output_round) 194 | 195 | @property 196 | def native_wind_speed(self): 197 | """Return the wind speed.""" 198 | wind_speed = float(self._weather_coordinator.data.current_weather.wind_speed) 199 | 200 | return round_if_needed(wind_speed, self._output_round) 201 | 202 | @property 203 | def native_dew_point(self): 204 | """Return the native dew point.""" 205 | dew_point = float(self._weather_coordinator.data.current_weather.due_point_temp) 206 | 207 | return round_if_needed(dew_point, self._output_round) 208 | 209 | @property 210 | def wind_bearing(self): 211 | """Return the wind bearing.""" 212 | return WIND_DIRECTIONS[ 213 | int(self._weather_coordinator.data.current_weather.wind_direction_id) 214 | ] 215 | 216 | @property 217 | def native_wind_gust_speed(self): 218 | """Return the gust wind speed.""" 219 | return ( 220 | int(self._weather_coordinator.data.current_weather.gust_speed) 221 | if self._weather_coordinator.data.current_weather.gust_speed 222 | else None 223 | ) 224 | 225 | @property 226 | def uv_index(self): 227 | """Return the wind bearing.""" 228 | return int(self._weather_coordinator.data.current_weather.u_v_index) 229 | 230 | @property 231 | def condition(self): 232 | """Return the weather condition.""" 233 | 234 | date_str = self._weather_coordinator.data.current_weather.json["forecast_time"] 235 | weather_code = get_hourly_weather_icon( 236 | date_str, 237 | self._weather_coordinator.data.current_weather.weather_code, 238 | "%Y-%m-%d %H:%M:%S", 239 | ) 240 | 241 | condition = WEATHER_CODE_TO_CONDITION.get(str(weather_code)) 242 | if not condition or condition == "Nothing": 243 | condition = WEATHER_CODE_TO_CONDITION.get( 244 | str(self._weather_coordinator.data.forecast.days[0].weather_code) 245 | ) 246 | return condition 247 | 248 | @property 249 | def description(self): 250 | """Return the weather description.""" 251 | description = self._weather_coordinator.data.current_weather.description 252 | if not description or description == "Nothing": 253 | description = self._weather_coordinator.data.forecast.days[0].weather 254 | return description 255 | 256 | def _forecast(self, hourly: bool) -> list[Forecast]: 257 | """Return the forecast array.""" 258 | data: list[Forecast] = [] 259 | 260 | if not hourly: 261 | data = [ 262 | Forecast( 263 | condition=WEATHER_CODE_TO_CONDITION.get( 264 | str(daily_forecast.weather_code) 265 | ), 266 | datetime=daily_forecast.date.isoformat(), 267 | native_temperature=daily_forecast.maximum_temperature, 268 | native_templow=daily_forecast.minimum_temperature, 269 | native_precipitation=sum( 270 | max(hour.rain or 0, 0) for hour in daily_forecast.hours 271 | ), 272 | ) 273 | for daily_forecast in self._weather_coordinator.data.forecast.days 274 | ] 275 | else: 276 | last_weather_code = None 277 | for daily_forecast in self._weather_coordinator.data.forecast.days: 278 | for hourly_forecast in daily_forecast.hours: 279 | # Skip negative weather codes 280 | try: 281 | weather_code_int = ( 282 | int(hourly_forecast.weather_code) 283 | if hourly_forecast.weather_code 284 | else 0 285 | ) 286 | except (ValueError, TypeError): 287 | weather_code_int = 0 288 | 289 | if ( 290 | hourly_forecast.weather_code 291 | and hourly_forecast.weather_code != "0" 292 | and weather_code_int >= 0 293 | ): 294 | last_weather_code = hourly_forecast.weather_code 295 | elif not last_weather_code: 296 | try: 297 | daily_weather_code_int = ( 298 | int(daily_forecast.weather_code) 299 | if daily_forecast.weather_code 300 | else 0 301 | ) 302 | except (ValueError, TypeError): 303 | daily_weather_code_int = 0 304 | 305 | if daily_forecast.weather_code and daily_weather_code_int >= 0: 306 | last_weather_code = daily_forecast.weather_code 307 | 308 | hourly_weather_code = get_hourly_weather_icon( 309 | hourly_forecast.hour, last_weather_code 310 | ) 311 | 312 | data.append( 313 | Forecast( 314 | condition=WEATHER_CODE_TO_CONDITION.get( 315 | str(hourly_weather_code) 316 | ), 317 | datetime=hourly_forecast.forecast_time.isoformat(), 318 | humidity=hourly_forecast.relative_humidity, 319 | native_temperature=hourly_forecast.precise_temperature, 320 | native_precipitation=max(hourly_forecast.rain or 0, 0), 321 | precipitation_probability=hourly_forecast.rain_chance, 322 | wind_bearing=WIND_DIRECTIONS[ 323 | hourly_forecast.wind_direction_id 324 | ], 325 | native_wind_speed=hourly_forecast.wind_speed, 326 | native_wind_gust_speed=hourly_forecast.gust_speed, 327 | uv_index=hourly_forecast.u_v_index, 328 | ) 329 | ) 330 | 331 | return data 332 | 333 | @property 334 | def forecast(self) -> list[Forecast]: 335 | """Return the forecast array.""" 336 | return self._forecast(hourly=(self._mode == FORECAST_MODE_HOURLY)) 337 | 338 | async def async_forecast_daily(self) -> list[Forecast]: 339 | """Return the daily forecast in native units.""" 340 | return self._forecast(False) 341 | 342 | async def async_forecast_hourly(self) -> list[Forecast]: 343 | """Return the hourly forecast in native units.""" 344 | return self._forecast(True) 345 | 346 | async def async_added_to_hass(self) -> None: 347 | """Connect to dispatcher listening for entity data notifications.""" 348 | self.async_on_remove( 349 | self._weather_coordinator.async_add_listener(self.async_write_ha_state) 350 | ) 351 | -------------------------------------------------------------------------------- /custom_components/ims/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for IMS Weather.""" 2 | 3 | import logging 4 | from datetime import timedelta 5 | 6 | import aiohttp 7 | import voluptuous as vol 8 | from math import radians, sin, cos, sqrt, atan2 9 | import socket 10 | 11 | from homeassistant import config_entries 12 | from homeassistant.const import ( 13 | CONF_MODE, 14 | CONF_NAME, 15 | CONF_MONITORED_CONDITIONS, 16 | ) 17 | from homeassistant.core import callback 18 | import homeassistant.helpers.config_validation as cv 19 | 20 | from .const import ( 21 | CONF_CITY, 22 | CONF_LANGUAGE, 23 | CONF_IMAGES_PATH, 24 | CONFIG_FLOW_VERSION, 25 | CONF_UPDATE_INTERVAL, 26 | DEFAULT_IMAGE_PATH, 27 | DEFAULT_FORECAST_MODE, 28 | DEFAULT_LANGUAGE, 29 | DEFAULT_NAME, 30 | DEFAULT_UPDATE_INTERVAL, 31 | DOMAIN, 32 | FORECAST_MODES, 33 | LANGUAGES, 34 | IMS_PLATFORMS, 35 | IMS_PLATFORM, 36 | ) 37 | from .sensor import SENSOR_DESCRIPTIONS_KEYS 38 | from .binary_sensor import BINARY_SENSOR_DESCRIPTIONS_KEYS 39 | 40 | ATTRIBUTION = "Powered by IMS Weather" 41 | _LOGGER = logging.getLogger(__name__) 42 | 43 | cities_data = None 44 | SENSOR_KEYS = SENSOR_DESCRIPTIONS_KEYS + BINARY_SENSOR_DESCRIPTIONS_KEYS 45 | 46 | 47 | class IMSWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 48 | """Config flow for IMSWeather.""" 49 | 50 | VERSION = CONFIG_FLOW_VERSION 51 | 52 | @staticmethod 53 | @callback 54 | def async_get_options_flow(config_entry): 55 | """Get the options flow for this handler.""" 56 | return IMSWeatherOptionsFlow(config_entry) 57 | 58 | async def async_step_user(self, user_input=None): 59 | """Handle a flow initialized by the user.""" 60 | errors = {} 61 | global cities_data 62 | 63 | if user_input is not None: 64 | city_id = user_input[CONF_CITY] 65 | city = cities_data[str(city_id)] 66 | user_input[CONF_CITY] = city 67 | language = user_input[CONF_LANGUAGE] 68 | forecast_mode = user_input[CONF_MODE] 69 | entity_name = user_input[CONF_NAME] 70 | # image_path = user_input[CONF_IMAGES_PATH] 71 | forecast_platform = user_input[IMS_PLATFORM] 72 | 73 | # Convert scan interval to timedelta 74 | if isinstance(user_input[CONF_UPDATE_INTERVAL], str): 75 | user_input[CONF_UPDATE_INTERVAL] = cv.time_period_str( 76 | user_input[CONF_UPDATE_INTERVAL] 77 | ) 78 | 79 | # Convert scan interval to number of minutes 80 | if isinstance(user_input[CONF_UPDATE_INTERVAL], timedelta): 81 | user_input[CONF_UPDATE_INTERVAL] = user_input[ 82 | CONF_UPDATE_INTERVAL 83 | ].total_minutes() 84 | 85 | # Unique value include to separate WeatherEntity/Sensor 86 | await self.async_set_unique_id( 87 | f"ims-{city_id}-{language}-{forecast_mode}-{forecast_platform}-{entity_name}" 88 | ) 89 | 90 | self._abort_if_unique_id_configured() 91 | 92 | api_status = "No API Call made" 93 | try: 94 | api_status = await _is_ims_api_online( 95 | self.hass, user_input[CONF_LANGUAGE], user_input[CONF_CITY] 96 | ) 97 | 98 | except Exception: 99 | _LOGGER.warning("IMS Weather Setup Error: HTTP Error: %s", api_status) 100 | errors["base"] = "API Error: " + api_status 101 | 102 | if not errors: 103 | return self.async_create_entry( 104 | title=user_input[CONF_NAME], data=user_input 105 | ) 106 | _LOGGER.warning(errors) 107 | 108 | # Step 1: Fetch the cities from an external URL based on the user's locale 109 | cities = await _get_localized_cities(self.hass) 110 | if not cities: 111 | errors["base"] = "cannot_retrieve_cities" 112 | return self.async_show_form( 113 | step_id="user", data_schema=vol.Schema({}), errors=errors 114 | ) 115 | 116 | # Step 2: Calculate the closest city based on Home Assistant's coordinates 117 | ha_latitude = self.hass.config.latitude 118 | ha_longitude = self.hass.config.longitude 119 | closest_city = _find_closest_city(cities, ha_latitude, ha_longitude) 120 | 121 | # Step 3: Create a selection field for cities 122 | city_options = {city_id: city["name"] for city_id, city in cities.items()} 123 | 124 | schema = vol.Schema( 125 | { 126 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, 127 | vol.Required(CONF_CITY, default=closest_city["lid"]): vol.In( 128 | city_options 129 | ), 130 | vol.Required(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In( 131 | LANGUAGES 132 | ), 133 | vol.Optional( 134 | CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL 135 | ): int, 136 | vol.Required(IMS_PLATFORM, default=[IMS_PLATFORMS[1]]): cv.multi_select( 137 | IMS_PLATFORMS 138 | ), 139 | vol.Required(CONF_MODE, default=DEFAULT_FORECAST_MODE): vol.In( 140 | FORECAST_MODES 141 | ), 142 | vol.Optional( 143 | CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS 144 | ): cv.multi_select(SENSOR_KEYS), 145 | vol.Required(CONF_IMAGES_PATH, default="/tmp"): cv.string, 146 | } 147 | ) 148 | 149 | return self.async_show_form(step_id="user", data_schema=schema, errors=errors) 150 | 151 | async def async_step_import(self, import_input=None): 152 | """Set the config entry up from yaml.""" 153 | config = import_input.copy() 154 | 155 | if CONF_NAME not in config: 156 | config[CONF_NAME] = DEFAULT_NAME 157 | if CONF_CITY not in config: 158 | config[CONF_CITY] = self.hass.config.city 159 | if CONF_LANGUAGE not in config: 160 | config[CONF_LANGUAGE] = self.hass.config.language 161 | if CONF_MODE not in config: 162 | config[CONF_MODE] = DEFAULT_FORECAST_MODE 163 | if IMS_PLATFORM not in config: 164 | config[IMS_PLATFORM] = None 165 | if CONF_LANGUAGE not in config: 166 | config[CONF_LANGUAGE] = DEFAULT_LANGUAGE 167 | if CONF_UPDATE_INTERVAL not in config: 168 | config[CONF_UPDATE_INTERVAL] = DEFAULT_UPDATE_INTERVAL 169 | if CONF_IMAGES_PATH not in config: 170 | config[CONF_IMAGES_PATH] = DEFAULT_IMAGE_PATH 171 | if CONF_MONITORED_CONDITIONS not in config: 172 | config[CONF_MONITORED_CONDITIONS] = SENSOR_KEYS 173 | return await self.async_step_user(config) 174 | 175 | 176 | supported_ims_languages = ["en", "he", "ar"] 177 | 178 | 179 | async def _is_ims_api_online(hass, language, city): 180 | forecast_url = "https://ims.gov.il/" + language + "/forecast_data/" + str(city) 181 | 182 | async with aiohttp.ClientSession( 183 | connector=aiohttp.TCPConnector(family=socket.AF_INET), raise_for_status=False 184 | ) as session: 185 | async with session.get(forecast_url) as resp: 186 | status = resp.status 187 | 188 | return status 189 | 190 | 191 | async def _get_localized_cities(hass): 192 | global cities_data 193 | if cities_data: 194 | return cities_data 195 | 196 | lang = hass.config.language 197 | if lang not in supported_ims_languages: 198 | lang = "en" 199 | locations_info_url = "https://ims.gov.il/" + lang + "/locations_info" 200 | 201 | try: 202 | async with aiohttp.ClientSession() as session: 203 | async with session.get(locations_info_url) as response: 204 | if response.status == 200: 205 | # Return the JSON data 206 | cities_json = await response.json() 207 | cities_data = cities_json.get("data", {}) 208 | else: 209 | # Handle HTTP errors 210 | _handle_http_error(response.status) 211 | return None 212 | except aiohttp.ClientError as e: 213 | # Handle connection issues, timeouts, etc. 214 | _handle_http_error(e) 215 | return None 216 | 217 | return cities_data 218 | 219 | 220 | @callback 221 | def _handle_http_error(self, error): 222 | """Handle HTTP errors.""" 223 | self.hass.logger.error(f"Error fetching data from URL: {error}") 224 | 225 | 226 | def _find_closest_city(cities, ha_latitude, ha_longitude): 227 | """Find the closest city based on the Home Assistant coordinates.""" 228 | 229 | def distance(lat1, lon1, lat2, lon2): 230 | # Calculate the distance between two lat/lon points (Haversine formula) 231 | R = 6371 # Radius of Earth in kilometers 232 | lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) 233 | dlat = lat2 - lat1 234 | dlon = lon2 - lon1 235 | a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2 236 | c = 2 * atan2(sqrt(a), sqrt(1 - a)) 237 | return R * c # Distance in kilometers 238 | 239 | closest_city = None 240 | closest_distance = float("inf") 241 | 242 | for city_id, city in cities.items(): 243 | city_lat = float(city["lat"]) 244 | city_lon = float(city["lon"]) 245 | dist = distance(ha_latitude, ha_longitude, city_lat, city_lon) 246 | 247 | if dist < closest_distance: 248 | closest_distance = dist 249 | closest_city = city 250 | 251 | if closest_distance > 10: 252 | return cities["1"] 253 | else: 254 | return closest_city 255 | 256 | 257 | class IMSWeatherOptionsFlow(config_entries.OptionsFlow): 258 | """Handle options.""" 259 | 260 | def __init__(self, config_entry): 261 | """Initialize options flow.""" 262 | self._config_entry = config_entry 263 | 264 | async def async_step_init(self, user_input=None): 265 | """Manage the options.""" 266 | global cities_data 267 | if not cities_data: 268 | cities_data = _get_localized_cities(self.hass) 269 | 270 | if user_input is not None: 271 | # entry = self.config_entry 272 | 273 | # _LOGGER.warning('async_step_init_Options') 274 | return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) 275 | 276 | return self.async_show_form( 277 | step_id="init", 278 | data_schema=vol.Schema( 279 | { 280 | vol.Optional( 281 | CONF_NAME, 282 | default=self._config_entry.options.get( 283 | CONF_NAME, 284 | self._config_entry.data.get(CONF_NAME, DEFAULT_NAME), 285 | ), 286 | ): str, 287 | vol.Optional( 288 | CONF_CITY, 289 | default=self._config_entry.options.get( 290 | CONF_CITY, 291 | self._config_entry.data.get(CONF_CITY, 1), 292 | ), 293 | ): int, 294 | vol.Optional( 295 | CONF_LANGUAGE, 296 | default=self._config_entry.options.get( 297 | CONF_LANGUAGE, 298 | self._config_entry.data.get( 299 | CONF_LANGUAGE, DEFAULT_LANGUAGE 300 | ), 301 | ), 302 | ): vol.In(LANGUAGES), 303 | vol.Required( 304 | IMS_PLATFORM, 305 | default=self._config_entry.options.get( 306 | IMS_PLATFORM, 307 | self._config_entry.data.get(IMS_PLATFORM, []), 308 | ), 309 | ): cv.multi_select(IMS_PLATFORMS), 310 | vol.Optional( 311 | CONF_MODE, 312 | default=self._config_entry.options.get( 313 | CONF_MODE, 314 | self._config_entry.data.get( 315 | CONF_MODE, DEFAULT_FORECAST_MODE 316 | ), 317 | ), 318 | ): vol.In(FORECAST_MODES), 319 | vol.Optional( 320 | CONF_UPDATE_INTERVAL, 321 | default=self._config_entry.options.get( 322 | CONF_UPDATE_INTERVAL, 323 | self._config_entry.data.get( 324 | CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL 325 | ), 326 | ), 327 | ): int, 328 | vol.Optional( 329 | CONF_MONITORED_CONDITIONS, 330 | default=self._config_entry.options.get( 331 | CONF_MONITORED_CONDITIONS, 332 | self._config_entry.data.get( 333 | CONF_MONITORED_CONDITIONS, SENSOR_KEYS 334 | ), 335 | ), 336 | ): cv.multi_select(SENSOR_KEYS), 337 | vol.Optional( 338 | CONF_IMAGES_PATH, 339 | default=self._config_entry.options.get( 340 | CONF_IMAGES_PATH, 341 | self._config_entry.data.get( 342 | CONF_IMAGES_PATH, DEFAULT_IMAGE_PATH 343 | ), 344 | ), 345 | ): str, 346 | } 347 | ), 348 | ) 349 | -------------------------------------------------------------------------------- /custom_components/ims/sensor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import types 3 | 4 | from homeassistant.components.sensor import ( 5 | SensorDeviceClass, 6 | SensorEntity, 7 | SensorStateClass, 8 | ) 9 | from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry 10 | from homeassistant.const import ( 11 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 12 | CONF_MONITORED_CONDITIONS, 13 | DEGREE, 14 | PERCENTAGE, 15 | UV_INDEX, 16 | UnitOfPrecipitationDepth, 17 | UnitOfSpeed, 18 | UnitOfTemperature, 19 | ) 20 | from homeassistant.core import HomeAssistant, callback 21 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 22 | 23 | from . import ImsEntity, ImsSensorEntityDescription 24 | from .const import ( 25 | DOMAIN, 26 | ENTRY_WEATHER_COORDINATOR, 27 | FIELD_NAME_DEW_POINT_TEMP, 28 | FIELD_NAME_FEELS_LIKE, 29 | FIELD_NAME_FORECAST_TIME, 30 | FIELD_NAME_HUMIDITY, 31 | FIELD_NAME_GUST_SPEED, 32 | FIELD_NAME_LOCATION, 33 | FIELD_NAME_PM10, 34 | FIELD_NAME_RAIN, 35 | FIELD_NAME_RAIN_CHANCE, 36 | FIELD_NAME_TEMPERATURE, 37 | FIELD_NAME_UV_INDEX, 38 | FIELD_NAME_UV_INDEX_MAX, 39 | FIELD_NAME_UV_LEVEL, 40 | FIELD_NAME_WIND_DIRECTION_ID, 41 | FIELD_NAME_WIND_SPEED, 42 | FORECAST_MODE, 43 | IMS_PLATFORM, 44 | IMS_PLATFORMS, 45 | IMS_SENSOR_KEY_PREFIX, 46 | TYPE_CITY, 47 | TYPE_CURRENT_UV_INDEX, 48 | TYPE_CURRENT_UV_LEVEL, 49 | TYPE_DEW_POINT_TEMP, 50 | TYPE_FEELS_LIKE, 51 | TYPE_FORECAST_DAY1, 52 | TYPE_FORECAST_DAY2, 53 | TYPE_FORECAST_DAY3, 54 | TYPE_FORECAST_DAY4, 55 | TYPE_FORECAST_DAY5, 56 | TYPE_FORECAST_DAY6, 57 | TYPE_FORECAST_DAY7, 58 | TYPE_FORECAST_PREFIX, 59 | TYPE_FORECAST_TIME, 60 | TYPE_FORECAST_TODAY, 61 | TYPE_GUST_SPEED, 62 | TYPE_HUMIDITY, 63 | TYPE_MAX_UV_INDEX, 64 | TYPE_PM10, 65 | TYPE_PRECIPITATION, 66 | TYPE_PRECIPITATION_PROBABILITY, 67 | TYPE_TEMPERATURE, 68 | TYPE_WIND_DIRECTION, 69 | TYPE_WIND_SPEED, 70 | UV_LEVEL_EXTREME, 71 | UV_LEVEL_HIGH, 72 | UV_LEVEL_LOW, 73 | UV_LEVEL_MODERATE, 74 | UV_LEVEL_VHIGH, 75 | WEATHER_CODE_TO_ICON, 76 | WIND_DIRECTIONS, 77 | FIELD_NAME_WARNING, 78 | TYPE_WEATHER_WARNINGS, 79 | DATETIME_FORMAT, 80 | ) 81 | from .utils import get_hourly_weather_icon 82 | 83 | sensor_keys = types.SimpleNamespace() 84 | sensor_keys.TYPE_CURRENT_UV_INDEX = IMS_SENSOR_KEY_PREFIX + TYPE_CURRENT_UV_INDEX 85 | sensor_keys.TYPE_CURRENT_UV_LEVEL = IMS_SENSOR_KEY_PREFIX + TYPE_CURRENT_UV_LEVEL 86 | sensor_keys.TYPE_DEW_POINT_TEMP = IMS_SENSOR_KEY_PREFIX + TYPE_DEW_POINT_TEMP 87 | sensor_keys.TYPE_MAX_UV_INDEX = IMS_SENSOR_KEY_PREFIX + TYPE_MAX_UV_INDEX 88 | sensor_keys.TYPE_GUST_SPEED = IMS_SENSOR_KEY_PREFIX + TYPE_GUST_SPEED 89 | sensor_keys.TYPE_CITY = IMS_SENSOR_KEY_PREFIX + TYPE_CITY 90 | sensor_keys.TYPE_TEMPERATURE = IMS_SENSOR_KEY_PREFIX + TYPE_TEMPERATURE 91 | sensor_keys.TYPE_HUMIDITY = IMS_SENSOR_KEY_PREFIX + TYPE_HUMIDITY 92 | sensor_keys.TYPE_FORECAST_TIME = IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_TIME 93 | sensor_keys.TYPE_FEELS_LIKE = IMS_SENSOR_KEY_PREFIX + TYPE_FEELS_LIKE 94 | sensor_keys.TYPE_PM10 = IMS_SENSOR_KEY_PREFIX + TYPE_PM10 95 | sensor_keys.TYPE_PRECIPITATION = IMS_SENSOR_KEY_PREFIX + TYPE_PRECIPITATION 96 | sensor_keys.TYPE_PRECIPITATION_PROBABILITY = ( 97 | IMS_SENSOR_KEY_PREFIX + TYPE_PRECIPITATION_PROBABILITY 98 | ) 99 | sensor_keys.TYPE_WIND_DIRECTION = IMS_SENSOR_KEY_PREFIX + TYPE_WIND_DIRECTION 100 | sensor_keys.TYPE_WEATHER_WARNINGS = IMS_SENSOR_KEY_PREFIX + TYPE_WEATHER_WARNINGS 101 | sensor_keys.TYPE_WIND_SPEED = IMS_SENSOR_KEY_PREFIX + TYPE_WIND_SPEED 102 | sensor_keys.TYPE_FORECAST_TODAY = ( 103 | IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_TODAY 104 | ) 105 | sensor_keys.TYPE_FORECAST_DAY1 = ( 106 | IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY1 107 | ) 108 | sensor_keys.TYPE_FORECAST_DAY2 = ( 109 | IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY2 110 | ) 111 | sensor_keys.TYPE_FORECAST_DAY3 = ( 112 | IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY3 113 | ) 114 | sensor_keys.TYPE_FORECAST_DAY4 = ( 115 | IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY4 116 | ) 117 | sensor_keys.TYPE_FORECAST_DAY5 = ( 118 | IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY5 119 | ) 120 | sensor_keys.TYPE_FORECAST_DAY6 = ( 121 | IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY6 122 | ) 123 | sensor_keys.TYPE_FORECAST_DAY7 = ( 124 | IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY7 125 | ) 126 | 127 | _LOGGER = logging.getLogger(__name__) 128 | 129 | SENSOR_DESCRIPTIONS: list[ImsSensorEntityDescription] = [ 130 | ImsSensorEntityDescription( 131 | key=IMS_SENSOR_KEY_PREFIX + TYPE_CURRENT_UV_INDEX, 132 | name="IMS Current UV Index", 133 | icon="mdi:weather-sunny", 134 | native_unit_of_measurement=UV_INDEX, 135 | state_class=SensorStateClass.MEASUREMENT, 136 | forecast_mode=FORECAST_MODE.CURRENT, 137 | field_name=FIELD_NAME_UV_INDEX, 138 | ), 139 | ImsSensorEntityDescription( 140 | key=IMS_SENSOR_KEY_PREFIX + TYPE_CURRENT_UV_LEVEL, 141 | name="IMS Current UV Level", 142 | icon="mdi:weather-sunny", 143 | forecast_mode=FORECAST_MODE.CURRENT, 144 | field_name=FIELD_NAME_UV_LEVEL, 145 | ), 146 | ImsSensorEntityDescription( 147 | key=IMS_SENSOR_KEY_PREFIX + TYPE_MAX_UV_INDEX, 148 | name="IMS Max UV Index", 149 | icon="mdi:weather-sunny", 150 | native_unit_of_measurement=UV_INDEX, 151 | state_class=SensorStateClass.MEASUREMENT, 152 | forecast_mode=FORECAST_MODE.CURRENT, 153 | field_name=FIELD_NAME_UV_INDEX_MAX, 154 | ), 155 | ImsSensorEntityDescription( 156 | key=IMS_SENSOR_KEY_PREFIX + TYPE_CITY, 157 | name="IMS City", 158 | icon="mdi:city", 159 | forecast_mode=FORECAST_MODE.CURRENT, 160 | field_name=FIELD_NAME_LOCATION, 161 | ), 162 | ImsSensorEntityDescription( 163 | key=IMS_SENSOR_KEY_PREFIX + TYPE_TEMPERATURE, 164 | name="IMS Temperature", 165 | icon="mdi:thermometer", 166 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 167 | device_class=SensorDeviceClass.TEMPERATURE, 168 | state_class=SensorStateClass.MEASUREMENT, 169 | forecast_mode=FORECAST_MODE.CURRENT, 170 | field_name=FIELD_NAME_TEMPERATURE, 171 | ), 172 | ImsSensorEntityDescription( 173 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FEELS_LIKE, 174 | name="IMS Feels Like", 175 | icon="mdi:water-percent", 176 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 177 | device_class=SensorDeviceClass.TEMPERATURE, 178 | state_class=SensorStateClass.MEASUREMENT, 179 | forecast_mode=FORECAST_MODE.CURRENT, 180 | field_name=FIELD_NAME_FEELS_LIKE, 181 | ), 182 | ImsSensorEntityDescription( 183 | key=IMS_SENSOR_KEY_PREFIX + TYPE_HUMIDITY, 184 | name="IMS Humidity", 185 | icon="mdi:weather-sunny", 186 | native_unit_of_measurement=PERCENTAGE, 187 | state_class=SensorStateClass.MEASUREMENT, 188 | forecast_mode=FORECAST_MODE.CURRENT, 189 | field_name=FIELD_NAME_HUMIDITY, 190 | ), 191 | ImsSensorEntityDescription( 192 | key=IMS_SENSOR_KEY_PREFIX + TYPE_WIND_DIRECTION, 193 | name="IMS Wind Direction", 194 | icon="mdi:weather-windy", 195 | native_unit_of_measurement=DEGREE, 196 | device_class=SensorDeviceClass.WIND_DIRECTION, 197 | state_class=SensorStateClass.MEASUREMENT_ANGLE, 198 | forecast_mode=FORECAST_MODE.CURRENT, 199 | field_name=FIELD_NAME_WIND_DIRECTION_ID, 200 | ), 201 | ImsSensorEntityDescription( 202 | key=IMS_SENSOR_KEY_PREFIX + TYPE_WIND_SPEED, 203 | name="IMS Wind Speed", 204 | icon="mdi:weather-windy", 205 | native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, 206 | device_class=SensorDeviceClass.SPEED, 207 | state_class=SensorStateClass.MEASUREMENT, 208 | forecast_mode=FORECAST_MODE.CURRENT, 209 | field_name=FIELD_NAME_WIND_SPEED, 210 | ), 211 | ImsSensorEntityDescription( 212 | key=IMS_SENSOR_KEY_PREFIX + TYPE_GUST_SPEED, 213 | name="IMS Gust Speed", 214 | icon="mdi:weather-dust", 215 | native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, 216 | device_class=SensorDeviceClass.SPEED, 217 | state_class=SensorStateClass.MEASUREMENT, 218 | forecast_mode=FORECAST_MODE.CURRENT, 219 | field_name=FIELD_NAME_GUST_SPEED, 220 | ), 221 | ImsSensorEntityDescription( 222 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_TIME, 223 | name="IMS Forecast Time", 224 | icon="mdi:calendar-clock", 225 | device_class=SensorDeviceClass.TIMESTAMP, 226 | forecast_mode=FORECAST_MODE.CURRENT, 227 | field_name=FIELD_NAME_FORECAST_TIME, 228 | ), 229 | ImsSensorEntityDescription( 230 | key=IMS_SENSOR_KEY_PREFIX + TYPE_PRECIPITATION, 231 | name="IMS Precipitation", 232 | icon="mdi:weather-pouring", 233 | native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, 234 | device_class=SensorDeviceClass.PRECIPITATION, 235 | state_class=SensorStateClass.MEASUREMENT, 236 | forecast_mode=FORECAST_MODE.CURRENT, 237 | field_name=FIELD_NAME_RAIN, 238 | ), 239 | ImsSensorEntityDescription( 240 | key=IMS_SENSOR_KEY_PREFIX + TYPE_PRECIPITATION_PROBABILITY, 241 | name="IMS Precipitation Probability", 242 | icon="mdi:cloud-percent", 243 | native_unit_of_measurement=PERCENTAGE, 244 | state_class=SensorStateClass.MEASUREMENT, 245 | forecast_mode=FORECAST_MODE.CURRENT, 246 | field_name=FIELD_NAME_RAIN_CHANCE, 247 | ), 248 | ImsSensorEntityDescription( 249 | key=IMS_SENSOR_KEY_PREFIX + TYPE_PM10, 250 | name="IMS PM10", 251 | icon="mdi:air-filter", 252 | device_class=SensorDeviceClass.PM10, 253 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 254 | state_class=SensorStateClass.MEASUREMENT, 255 | forecast_mode=FORECAST_MODE.CURRENT, 256 | field_name=FIELD_NAME_PM10, 257 | ), 258 | ImsSensorEntityDescription( 259 | key=IMS_SENSOR_KEY_PREFIX + TYPE_DEW_POINT_TEMP, 260 | name="IMS Dew Point", 261 | icon="mdi:water-circle", 262 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 263 | device_class=SensorDeviceClass.TEMPERATURE, 264 | state_class=SensorStateClass.MEASUREMENT, 265 | forecast_mode=FORECAST_MODE.CURRENT, 266 | field_name=FIELD_NAME_DEW_POINT_TEMP, 267 | ), 268 | ImsSensorEntityDescription( 269 | key=IMS_SENSOR_KEY_PREFIX + TYPE_WEATHER_WARNINGS, 270 | name="IMS Weather Warnings", 271 | icon="mdi:weather-cloudy-alert", 272 | forecast_mode=FORECAST_MODE.CURRENT, 273 | field_name=FIELD_NAME_WARNING, 274 | ), 275 | ImsSensorEntityDescription( 276 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_TODAY, 277 | name="IMS Forecast Today", 278 | icon="mdi:weather-sunny", 279 | ), 280 | ImsSensorEntityDescription( 281 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY1, 282 | name="IMS Forecast Day1", 283 | icon="mdi:weather-sunny", 284 | ), 285 | ImsSensorEntityDescription( 286 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY2, 287 | name="IMS Forecast Day2", 288 | icon="mdi:weather-windy", 289 | ), 290 | ImsSensorEntityDescription( 291 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY3, 292 | name="IMS Forecast Day3", 293 | icon="mdi:weather-sunny", 294 | ), 295 | ImsSensorEntityDescription( 296 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY4, 297 | name="IMS Forecast Day4", 298 | icon="mdi:weather-sunny", 299 | ), 300 | ImsSensorEntityDescription( 301 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY5, 302 | name="IMS Forecast Day5", 303 | icon="mdi:weather-sunny", 304 | ), 305 | ImsSensorEntityDescription( 306 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY6, 307 | name="IMS Forecast Day6", 308 | icon="mdi:weather-sunny", 309 | ), 310 | ImsSensorEntityDescription( 311 | key=IMS_SENSOR_KEY_PREFIX + TYPE_FORECAST_PREFIX + TYPE_FORECAST_DAY7, 312 | name="IMS Forecast Day7", 313 | icon="mdi:weather-sunny", 314 | ), 315 | ] 316 | 317 | SENSOR_DESCRIPTIONS_DICT = {desc.key: desc for desc in SENSOR_DESCRIPTIONS} 318 | SENSOR_DESCRIPTIONS_KEYS = [desc.key for desc in SENSOR_DESCRIPTIONS] 319 | 320 | weather = None 321 | 322 | 323 | async def async_setup_platform( 324 | hass, config_entry, async_add_entities, discovery_info=None 325 | ): 326 | _LOGGER.warning( 327 | "Configuration of IMS Weather sensor in YAML is deprecated " 328 | "Your existing configuration has been imported into the UI automatically " 329 | "and can be safely removed from your configuration.yaml file" 330 | ) 331 | 332 | # Define as a sensor platform 333 | config_entry[IMS_PLATFORM] = [IMS_PLATFORMS[0]] 334 | 335 | hass.async_create_task( 336 | hass.config_entries.flow.async_init( 337 | DOMAIN, context={"source": SOURCE_IMPORT}, data=config_entry 338 | ) 339 | ) 340 | 341 | 342 | async def async_setup_entry( 343 | hass: HomeAssistant, 344 | config_entry: ConfigEntry, 345 | async_add_entities: AddEntitiesCallback, 346 | ) -> None: 347 | """Set up IMS Weather sensor entities based on a config entry.""" 348 | 349 | domain_data = hass.data[DOMAIN][config_entry.entry_id] 350 | conditions = domain_data[CONF_MONITORED_CONDITIONS] 351 | weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] 352 | 353 | # Add IMS Sensors 354 | sensors: list[ImsSensor] = [] 355 | 356 | if conditions is None: 357 | # If a problem happens - create all sensors 358 | conditions = SENSOR_DESCRIPTIONS_KEYS 359 | 360 | for condition in conditions: 361 | if condition in SENSOR_DESCRIPTIONS_KEYS: 362 | description = SENSOR_DESCRIPTIONS_DICT[condition] 363 | sensors.append(ImsSensor(weather_coordinator, description)) 364 | 365 | async_add_entities(sensors, update_before_add=True) 366 | 367 | 368 | def generate_single_warning_string(warning): 369 | return f"{warning.valid_from.strftime(DATETIME_FORMAT)} - {warning.valid_to.strftime(DATETIME_FORMAT)}\n{warning.text_full}" 370 | 371 | 372 | def generate_warnings_extra_state_attributes(warnings): 373 | warnings_str = [] 374 | for warning in warnings: 375 | warnings_str.append(generate_single_warning_string(warning)) 376 | 377 | attributes = {"warnings": warnings_str} 378 | return attributes 379 | 380 | 381 | def generate_forecast_extra_state_attributes(daily_forecast): 382 | attributes = { 383 | "minimum_temperature": { 384 | "value": daily_forecast.minimum_temperature, 385 | "unit": UnitOfTemperature.CELSIUS, 386 | }, 387 | "maximum_temperature": { 388 | "value": daily_forecast.maximum_temperature, 389 | "unit": UnitOfTemperature.CELSIUS, 390 | }, 391 | "maximum_uvi": {"value": daily_forecast.maximum_uvi, "unit": UV_INDEX}, 392 | "weather": { 393 | "value": daily_forecast.weather, 394 | "icon": WEATHER_CODE_TO_ICON.get( 395 | str(daily_forecast.weather_code), "mdi:weather-sunny" 396 | ), 397 | }, 398 | "description": {"value": daily_forecast.description}, 399 | "date": {"value": daily_forecast.date.strftime("%Y/%m/%d")}, 400 | } 401 | 402 | last_weather_code = None 403 | last_weather_status = None 404 | for hour in daily_forecast.hours: 405 | if hour.weather and hour.weather != "Nothing": 406 | last_weather_status = hour.weather 407 | elif not last_weather_status: 408 | last_weather_status = daily_forecast.weather 409 | 410 | if hour.weather_code and hour.weather_code != "0": 411 | last_weather_code = hour.weather_code 412 | elif not last_weather_code: 413 | last_weather_code = daily_forecast.weather_code 414 | 415 | hourly_weather_code = get_hourly_weather_icon(hour.hour, last_weather_code) 416 | 417 | attributes[hour.hour] = { 418 | "weather": { 419 | "value": last_weather_status, 420 | "icon": WEATHER_CODE_TO_ICON.get(str(hourly_weather_code)), 421 | }, 422 | "temperature": { 423 | "value": hour.precise_temperature or hour.temperature, 424 | "unit": UnitOfTemperature.CELSIUS, 425 | }, 426 | } 427 | 428 | return attributes 429 | 430 | 431 | class ImsSensor(ImsEntity, SensorEntity): 432 | """Representation of an IMS sensor.""" 433 | 434 | @callback 435 | def _update_from_latest_data(self) -> None: 436 | """Update the state.""" 437 | data = self.coordinator.data 438 | 439 | if ( 440 | self.entity_description.forecast_mode == FORECAST_MODE.DAILY 441 | or self.entity_description.forecast_mode == FORECAST_MODE.HOURLY 442 | ): 443 | if not data or not data.forecast: 444 | _LOGGER.warning( 445 | "For %s - no data.forecast", self.entity_description.key 446 | ) 447 | self._attr_native_value = None 448 | return 449 | elif self.entity_description.forecast_mode == FORECAST_MODE.CURRENT: 450 | if not data or not data.current_weather: 451 | _LOGGER.warning( 452 | "For %s - no data.current_weather", self.entity_description.key 453 | ) 454 | self._attr_native_value = None 455 | return 456 | 457 | match self.entity_description.key: 458 | case sensor_keys.TYPE_CURRENT_UV_LEVEL: 459 | match data.current_weather.u_v_level: 460 | case "E": 461 | self._attr_native_value = UV_LEVEL_EXTREME 462 | case "V": 463 | self._attr_native_value = UV_LEVEL_VHIGH 464 | case "H": 465 | self._attr_native_value = UV_LEVEL_HIGH 466 | case "M": 467 | self._attr_native_value = UV_LEVEL_MODERATE 468 | case _: 469 | self._attr_native_value = UV_LEVEL_LOW 470 | 471 | case sensor_keys.TYPE_CURRENT_UV_INDEX: 472 | self._attr_native_value = data.current_weather.u_v_index 473 | 474 | case sensor_keys.TYPE_MAX_UV_INDEX: 475 | self._attr_native_value = data.current_weather.u_v_i_max 476 | 477 | case sensor_keys.TYPE_GUST_SPEED: 478 | self._attr_native_value = data.current_weather.gust_speed 479 | 480 | case sensor_keys.TYPE_CITY: 481 | _LOGGER.debug( 482 | "Location: %s, entity: %s", 483 | data.current_weather.location, 484 | self.entity_description.key, 485 | ) 486 | self._attr_native_value = data.current_weather.location 487 | 488 | case sensor_keys.TYPE_TEMPERATURE: 489 | self._attr_native_value = data.current_weather.temperature 490 | 491 | case sensor_keys.TYPE_DEW_POINT_TEMP: 492 | self._attr_native_value = data.current_weather.due_point_temp 493 | 494 | case sensor_keys.TYPE_FEELS_LIKE: 495 | self._attr_native_value = data.current_weather.feels_like 496 | 497 | case sensor_keys.TYPE_HUMIDITY: 498 | self._attr_native_value = data.current_weather.humidity 499 | 500 | case sensor_keys.TYPE_PM10: 501 | self._attr_native_value = data.current_weather.pm10 502 | 503 | case sensor_keys.TYPE_PRECIPITATION: 504 | self._attr_native_value = ( 505 | data.current_weather.rain 506 | if (data.current_weather.rain and data.current_weather.rain > 0.0) 507 | else 0.0 508 | ) 509 | 510 | case sensor_keys.TYPE_PRECIPITATION_PROBABILITY: 511 | self._attr_native_value = data.current_weather.rain_chance 512 | 513 | case sensor_keys.TYPE_FORECAST_TIME: 514 | self._attr_native_value = ( 515 | data.current_weather.forecast_time.astimezone() 516 | ) 517 | 518 | case sensor_keys.TYPE_WIND_DIRECTION: 519 | self._attr_native_value = WIND_DIRECTIONS[ 520 | int(data.current_weather.wind_direction_id) 521 | ] 522 | 523 | case sensor_keys.TYPE_WIND_SPEED: 524 | self._attr_native_value = data.current_weather.wind_speed 525 | 526 | case sensor_keys.TYPE_WEATHER_WARNINGS: 527 | self._attr_native_value = len(data.warnings) 528 | self._attr_extra_state_attributes = ( 529 | generate_warnings_extra_state_attributes(data.warnings) 530 | ) 531 | 532 | case ( 533 | sensor_keys.TYPE_FORECAST_TODAY 534 | | sensor_keys.TYPE_FORECAST_DAY1 535 | | sensor_keys.TYPE_FORECAST_DAY2 536 | | sensor_keys.TYPE_FORECAST_DAY3 537 | | sensor_keys.TYPE_FORECAST_DAY4 538 | | sensor_keys.TYPE_FORECAST_DAY5 539 | | sensor_keys.TYPE_FORECAST_DAY6 540 | | sensor_keys.TYPE_FORECAST_DAY7 541 | ): 542 | day_index = ( 543 | 0 544 | if self.entity_description.key == sensor_keys.TYPE_FORECAST_TODAY 545 | else int(self.entity_description.key[-1]) 546 | ) 547 | if day_index < len(data.forecast.days): 548 | daily_forecast = data.forecast.days[day_index] 549 | self._attr_native_value = daily_forecast.day 550 | self._attr_extra_state_attributes = ( 551 | generate_forecast_extra_state_attributes(daily_forecast) 552 | ) 553 | self._attr_icon = WEATHER_CODE_TO_ICON.get( 554 | str(daily_forecast.weather_code), "mdi:weather-sunny" 555 | ) 556 | 557 | case _: 558 | self._attr_native_value = None 559 | --------------------------------------------------------------------------------