初始化config文件

This commit is contained in:
2026-08-24 21:55:33 +08:00
commit 80a05d172e
2983 changed files with 1221334 additions and 0 deletions
@@ -0,0 +1,111 @@
import asyncio
import logging
from typing import Optional
from .heweather.heweather_cert import HeWeatherCert
from .heweather.const import (
DOMAIN,
CONF_STORAGE_PATH,
)
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.helpers import entity_registry as er
SUPPORTED_PLATFORMS = [Platform.WEATHER, Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
async def cleanup_duplicate_entities_on_startup(hass: HomeAssistant, config_entry: ConfigEntry):
"""在启动时清理重复的实体"""
entity_registry = er.async_get(hass)
# 获取所有与此集成相关的实体,按传感器类型分组
entities_by_type = {}
for entity_id, entity in entity_registry.entities.items():
if entity.config_entry_id == config_entry.entry_id and entity.platform == DOMAIN:
if entity.unique_id:
# 提取传感器类型(unique_id的第一部分)
try:
sensor_type = entity.unique_id.split('_')[0] + '_' + entity.unique_id.split('_')[1]
if sensor_type not in entities_by_type:
entities_by_type[sensor_type] = []
entities_by_type[sensor_type].append((entity_id, entity))
except IndexError:
# 如果unique_id格式不符合预期,跳过
continue
# 检查每种传感器类型是否有重复
entities_to_remove = []
for sensor_type, entities in entities_by_type.items():
if len(entities) > 1:
# 有重复实体,保留最新的(通过entity_id排序,通常较新的ID会更大)
entities.sort(key=lambda x: x[0]) # 按entity_id排序
# 删除除最后一个之外的所有实体
for entity_id, entity in entities[:-1]:
entities_to_remove.append(entity_id)
_LOGGER.info(f"Found duplicate entity for {sensor_type}: {entity_id}")
# 删除重复实体
for entity_id in entities_to_remove:
_LOGGER.info(f"Removing duplicate entity on startup: {entity_id}")
entity_registry.async_remove(entity_id)
if entities_to_remove:
_LOGGER.info(f"Cleaned up {len(entities_to_remove)} duplicate entities on startup")
async def async_setup(hass: HomeAssistant, hass_config: dict) -> bool:
# pylint: disable=unused-argument
hass.data.setdefault(DOMAIN, {})
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
# Get running loop
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
if not loop:
raise Exception('loop is None')
# HeWeather Certification
cert: Optional[HeWeatherCert] = hass.data[DOMAIN].get("heweather_cert", None)
if not cert:
cert = HeWeatherCert(root_path=config_entry.data.get(CONF_STORAGE_PATH), loop=loop)
hass.data[DOMAIN]["heweather_cert"] = cert
_LOGGER.info("create heweather cert instance")
await hass.config_entries.async_forward_entry_setups(config_entry, SUPPORTED_PLATFORMS)
# 清理重复的实体(在平台设置完成后)
await cleanup_duplicate_entities_on_startup(hass, config_entry)
return True
async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Update a given config entry."""
await hass.config_entries.async_reload(config_entry.entry_id)
# 在重新加载后清理重复实体
await cleanup_duplicate_entities_on_startup(hass, config_entry)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, SUPPORTED_PLATFORMS
)
return unload_ok
async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
heweather_cert: HeWeatherCert = hass.data[DOMAIN]['heweather_cert']
await heweather_cert.del_key_async()
hass.data.pop(DOMAIN, None)
return True
@@ -0,0 +1,707 @@
import logging
import asyncio
from typing import Optional
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import entity_registry as er
from .heweather.const import (
DOMAIN,
DEFAULT_NAME,
CONF_AUTH_METHOD,
CONF_LONGITUDE,
CONF_LATITUDE,
CONF_HOST,
CONF_KEY,
CONF_STORAGE_PATH,
CONF_JWT_SUB,
CONF_JWT_KID,
CONF_DISASTERLEVEL,
CONF_DISASTERMSG,
CONF_SENSOR_LIST,
DEFAULT_HOST,
DEFAULT_AUTH_METHOD,
AUTH_METHOD,
DEFAULT_DISASTER_LEVEL_CONF,
DISASTER_LEVEL_CONF,
DEFAULT_DISASTER_MSG,
DISASTER_MSG
)
from .heweather.heweather_cert import HeWeatherCert
_LOGGER = logging.getLogger(__name__)
async def migrate_entities_for_location_change(hass, config_entry, old_longitude=None, old_latitude=None, new_longitude=None, new_latitude=None):
"""迁移实体到新的经纬度,保持实体ID和配置"""
if old_longitude is None or old_latitude is None or new_longitude is None or new_latitude is None:
return
entity_registry = er.async_get(hass)
# 获取所有与此集成相关的旧实体
entities_to_migrate = []
old_unique_id_suffix = f"_{old_longitude}_{old_latitude}"
new_unique_id_suffix = f"_{new_longitude}_{new_latitude}"
for entity_id, entity in entity_registry.entities.items():
if entity.config_entry_id == config_entry.entry_id and entity.platform == DOMAIN:
if entity.unique_id and old_unique_id_suffix in entity.unique_id:
entities_to_migrate.append((entity_id, entity))
# 迁移实体:更新unique_id而不删除实体
migrated_count = 0
for entity_id, entity in entities_to_migrate:
# 生成新的unique_id
new_unique_id = entity.unique_id.replace(old_unique_id_suffix, new_unique_id_suffix)
try:
# 更新实体的unique_id
entity_registry.async_update_entity(
entity_id,
new_unique_id=new_unique_id
)
_LOGGER.info(f"Migrated entity {entity_id}: {entity.unique_id} -> {new_unique_id}")
migrated_count += 1
except Exception as e:
_LOGGER.error(f"Failed to migrate entity {entity_id}: {e}")
# 如果迁移失败,则删除旧实体让系统重新创建
entity_registry.async_remove(entity_id)
_LOGGER.info(f"Removed entity {entity_id} due to migration failure")
if migrated_count > 0:
_LOGGER.info(f"Successfully migrated {migrated_count} entities to new location")
async def cleanup_old_entities(hass, config_entry, old_longitude=None, old_latitude=None):
"""清理旧的实体,当经纬度发生变化时(备用方法)"""
if old_longitude is None or old_latitude is None:
return
entity_registry = er.async_get(hass)
# 获取所有与此集成相关的实体
entities_to_remove = []
for entity_id, entity in entity_registry.entities.items():
if entity.config_entry_id == config_entry.entry_id and entity.platform == DOMAIN:
# 检查是否是旧的实体(基于旧的经纬度)
old_unique_id_suffix = f"_{old_longitude}_{old_latitude}"
if entity.unique_id and old_unique_id_suffix in entity.unique_id:
entities_to_remove.append(entity_id)
# 删除旧实体
for entity_id in entities_to_remove:
_LOGGER.info(f"Removing old entity: {entity_id}")
entity_registry.async_remove(entity_id)
if entities_to_remove:
_LOGGER.info(f"Cleaned up {len(entities_to_remove)} old entities")
async def cleanup_duplicate_entities(hass, config_entry):
"""清理重复的实体,保留最新的"""
entity_registry = er.async_get(hass)
# 获取所有与此集成相关的实体,按传感器类型分组
entities_by_type = {}
for entity_id, entity in entity_registry.entities.items():
if entity.config_entry_id == config_entry.entry_id and entity.platform == DOMAIN:
if entity.unique_id:
# 提取传感器类型(unique_id的第一部分)
sensor_type = entity.unique_id.split('_')[0] + '_' + entity.unique_id.split('_')[1]
if sensor_type not in entities_by_type:
entities_by_type[sensor_type] = []
entities_by_type[sensor_type].append((entity_id, entity))
# 检查每种传感器类型是否有重复
entities_to_remove = []
for sensor_type, entities in entities_by_type.items():
if len(entities) > 1:
# 有重复实体,保留最新的(通过entity_id排序,通常较新的ID会更大)
entities.sort(key=lambda x: x[0]) # 按entity_id排序
# 删除除最后一个之外的所有实体
for entity_id, entity in entities[:-1]:
entities_to_remove.append(entity_id)
_LOGGER.info(f"Found duplicate entity for {sensor_type}: {entity_id}")
# 删除重复实体
for entity_id in entities_to_remove:
_LOGGER.info(f"Removing duplicate entity: {entity_id}")
entity_registry.async_remove(entity_id)
if entities_to_remove:
_LOGGER.info(f"Cleaned up {len(entities_to_remove)} duplicate entities")
def validate_longitude(lon: str) -> bool:
try:
lon_float = float(lon)
return -180 <= lon_float <= 180
except ValueError:
return False
def validate_latitude(lat: str) -> bool:
try:
lat_float = float(lat)
return -90 <= lat_float <= 90
except ValueError:
return False
class HeWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
MINOR_VERSION = 1
SUPPORT_MULTIPLE_ENTRIES = True
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return HeWeatherOptionsFlow(config_entry)
_main_loop: asyncio.AbstractEventLoop
_heweather_cert: HeWeatherCert
_storage_path: str
_auth_method: str
_host: str
_key: str
_jwt_pubkey: str
_jwt_sub: str
_jwt_kid: str
_longitude: str
_latitude: str
_disasterlevel: str
_disastermsg: str
def __init__(self):
self._main_loop = asyncio.get_running_loop()
self._storage_path = ''
self._auth_method = DEFAULT_AUTH_METHOD
self._host = DEFAULT_HOST
self._key = ''
self._jwt_pubkey = ''
self._jwt_sub = ''
self._jwt_kid = ''
self._longitude = ''
self._latitude = ''
self._disasterlevel = DEFAULT_DISASTER_LEVEL_CONF
self._disastermsg = DEFAULT_DISASTER_MSG
async def async_step_user(
self, user_input: Optional[dict] = None
):
#if self._async_current_entries():
# return self.async_abort(reason="single_instance_allowed")
self.hass.data.setdefault(DOMAIN, {})
if not self._storage_path:
self._storage_path = self.hass.config.path('.storage', DOMAIN)
# HeWeather Certification
self._heweather_cert = self.hass.data[DOMAIN].get('heweather_cert', None)
if not self._heweather_cert:
self._heweather_cert = HeWeatherCert(
root_path=self._storage_path,
loop=self._main_loop)
self.hass.data[DOMAIN]['heweather_cert'] = self._heweather_cert
_LOGGER.info(
'async_step_user, create heweather cert, %s', self._storage_path)
return await self.async_step_auth_method_config(user_input)
async def async_step_auth_method_config(
self, user_input: Optional[dict] = None
):
if user_input:
self._auth_method = user_input.get("auth_method", self._auth_method)
if self._auth_method == "key":
return await self.async_step_auth_apikey_config()
else:
return await self.async_step_auth_jwt_config()
return await self.__show_auth_method_config_form("")
async def __show_auth_method_config_form(self, reason: str):
return self.async_show_form(
step_id="auth_method_config",
data_schema=vol.Schema({
vol.Required(
"auth_method",
default=self._auth_method
): vol.In(AUTH_METHOD)
}),
errors={'base': reason},
last_step=False
)
async def async_step_auth_apikey_config(
self, user_input: Optional[dict] = None
):
if user_input:
if user_input.get("key", self._key) == "":
return await self.__show_auth_apikey_config_form("key is empty")
elif user_input.get("host", None) == "":
return await self.__show_auth_apikey_config_form("host is empty")
else:
self._key = user_input.get("key", self._key)
self._host = user_input.get("host", self._host)
return await self.async_step_location_config()
return await self.__show_auth_apikey_config_form("")
async def __show_auth_apikey_config_form(self, reason: str):
return self.async_show_form(
step_id="auth_apikey_config",
data_schema=vol.Schema({
vol.Required(
"key",
default=self._key
): str,
vol.Required(
"host",
default=self._host
): str
}),
errors={'base': reason},
last_step=False
)
async def async_step_auth_jwt_config(
self, user_input: Optional[dict] = None
):
if user_input:
if user_input.get("jwt_sub", self._key) == "":
return await self.__show_auth_apikey_config_form("jwt_sub is empty")
elif user_input.get("jwt_kid", None) == "":
return await self.__show_auth_apikey_config_form("jwt_kid is empty")
elif user_input.get("host", None) == "":
return await self.__show_auth_apikey_config_form("host is empty")
else:
self._jwt_sub = user_input.get("jwt_sub", self._jwt_sub)
self._jwt_kid = user_input.get("jwt_kid", self._jwt_kid)
self._host = user_input.get("host", self._host)
return await self.async_step_location_config()
await self._heweather_cert.gen_key_async()
self._jwt_pubkey = await self._heweather_cert.get_pub_key_async()
return await self.__show_auth_jwt_config_form("")
async def __show_auth_jwt_config_form(self, reason: str):
return self.async_show_form(
step_id="auth_jwt_config",
data_schema=vol.Schema({
vol.Required(
"jwt_sub",
default=self._jwt_sub
): str,
vol.Required(
"jwt_kid",
default=self._jwt_kid
): str,
vol.Required(
"host",
default=self._host
): str
}),
description_placeholders={
"jwt_pubkey": self._jwt_pubkey,
},
errors={'base': reason},
last_step=False
)
async def async_step_location_config(
self, user_input: Optional[dict] = None
):
if user_input:
longitude = user_input.get("longitude", "")
latitude = user_input.get("latitude", "")
if not longitude or not latitude:
return await self.__show_location_config_form("empty_location")
if not validate_longitude(longitude):
return await self.__show_location_config_form("invalid_longitude")
if not validate_latitude(latitude):
return await self.__show_location_config_form("invalid_latitude")
self._longitude = f"{float(longitude):.4f}"
self._latitude = f"{float(latitude):.4f}"
return await self.async_step_disaster_config()
return await self.__show_location_config_form("")
async def __show_location_config_form(self, reason: str):
return self.async_show_form(
step_id="location_config",
data_schema=vol.Schema({
vol.Required(
"longitude",
default=self._longitude
): str,
vol.Required(
"latitude",
default=self._latitude
): str,
}),
errors={'base': reason},
last_step=False
)
async def async_step_disaster_config(
self, user_input: Optional[dict] = None
):
if user_input:
self._disasterlevel = user_input.get("disasterlevel", self._disasterlevel)
self._disastermsg = user_input.get("disastermsg", self._disastermsg)
return await self.config_flow_done()
return await self.__show_disaster_config_form("")
async def __show_disaster_config_form(self, reason: str):
return self.async_show_form(
step_id="disaster_config",
data_schema=vol.Schema({
vol.Required(
"disasterlevel",
default=self._disasterlevel
): vol.In(DISASTER_LEVEL_CONF),
vol.Required(
"disastermsg",
default=self._disastermsg
): vol.In(DISASTER_MSG),
}),
errors={'base': reason},
last_step=False
)
async def config_flow_done(self):
return self.async_create_entry(
title=DEFAULT_NAME,
data={
CONF_AUTH_METHOD: self._auth_method,
CONF_KEY: self._key,
CONF_STORAGE_PATH: self._storage_path,
CONF_JWT_SUB: self._jwt_sub,
CONF_JWT_KID: self._jwt_kid,
CONF_HOST: self._host,
CONF_LONGITUDE: self._longitude,
CONF_LATITUDE: self._latitude,
CONF_DISASTERLEVEL: self._disasterlevel,
CONF_DISASTERMSG: self._disastermsg,
})
class HeWeatherOptionsFlow(config_entries.OptionsFlow):
"""Handle options flow for HeWeather."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self._config_entry = config_entry
self._auth_method = config_entry.data.get(CONF_AUTH_METHOD, DEFAULT_AUTH_METHOD)
self._jwt_pubkey = ""
self._heweather_cert = None
# Initialize variables for different auth methods
self._key = config_entry.data.get(CONF_KEY, "")
self._host = config_entry.data.get(CONF_HOST, DEFAULT_HOST)
self._jwt_sub = config_entry.data.get(CONF_JWT_SUB, "")
self._jwt_kid = config_entry.data.get(CONF_JWT_KID, "")
# Initialize location and disaster config
self._longitude = config_entry.data.get(CONF_LONGITUDE, "")
self._latitude = config_entry.data.get(CONF_LATITUDE, "")
self._disasterlevel = config_entry.data.get(CONF_DISASTERLEVEL, DEFAULT_DISASTER_LEVEL_CONF)
self._disastermsg = config_entry.data.get(CONF_DISASTERMSG, DEFAULT_DISASTER_MSG)
async def async_step_init(self, user_input: Optional[dict] = None):
"""Manage the options."""
return await self.async_step_auth_method_config()
async def async_step_auth_method_config(self, user_input: Optional[dict] = None):
"""Handle authentication method configuration."""
if user_input is not None:
self._auth_method = user_input.get("auth_method", self._auth_method)
if self._auth_method == "key":
return await self.async_step_auth_apikey_config()
else:
return await self.async_step_auth_jwt_config()
return self.async_show_form(
step_id="auth_method_config",
data_schema=vol.Schema({
vol.Required(
"auth_method",
default=self._auth_method
): vol.In(AUTH_METHOD)
}),
)
async def async_step_auth_apikey_config(self, user_input: Optional[dict] = None):
"""Handle API key authentication configuration."""
if user_input is not None:
# Validate inputs
if not user_input.get("key", "").strip():
return self.async_show_form(
step_id="auth_apikey_config",
data_schema=self._get_apikey_schema(),
errors={"base": "key is empty"}
)
if not user_input.get("host", "").strip():
return self.async_show_form(
step_id="auth_apikey_config",
data_schema=self._get_apikey_schema(),
errors={"base": "host is empty"}
)
# Store auth data and proceed to location config
self._key = user_input.get("key", "")
self._host = user_input.get("host", DEFAULT_HOST)
return await self.async_step_location_config()
return self.async_show_form(
step_id="auth_apikey_config",
data_schema=self._get_apikey_schema(),
)
def _get_apikey_schema(self):
"""Get API key configuration schema."""
current_key = self._config_entry.data.get(CONF_KEY, "")
current_host = self._config_entry.data.get(CONF_HOST, DEFAULT_HOST)
return vol.Schema({
vol.Required(
"key",
default=current_key
): str,
vol.Required(
"host",
default=current_host
): str
})
async def async_step_auth_jwt_config(self, user_input: Optional[dict] = None):
"""Handle JWT authentication configuration."""
if user_input is not None:
# Validate inputs
if not user_input.get("jwt_sub", "").strip():
return self.async_show_form(
step_id="auth_jwt_config",
data_schema=self._get_jwt_schema(),
description_placeholders={"jwt_pubkey": self._jwt_pubkey},
errors={"base": "jwt_sub is empty"}
)
if not user_input.get("jwt_kid", "").strip():
return self.async_show_form(
step_id="auth_jwt_config",
data_schema=self._get_jwt_schema(),
description_placeholders={"jwt_pubkey": self._jwt_pubkey},
errors={"base": "jwt_kid is empty"}
)
if not user_input.get("host", "").strip():
return self.async_show_form(
step_id="auth_jwt_config",
data_schema=self._get_jwt_schema(),
description_placeholders={"jwt_pubkey": self._jwt_pubkey},
errors={"base": "host is empty"}
)
# Store auth data and proceed to location config
self._jwt_sub = user_input.get("jwt_sub", "")
self._jwt_kid = user_input.get("jwt_kid", "")
self._host = user_input.get("host", DEFAULT_HOST)
return await self.async_step_location_config()
# Get HeWeather cert instance and public key
if not self._heweather_cert:
# Get the cert instance from hass data
storage_path = self._config_entry.data.get(CONF_STORAGE_PATH)
if storage_path:
self.hass.data.setdefault(DOMAIN, {})
self._heweather_cert = self.hass.data[DOMAIN].get('heweather_cert', None)
if not self._heweather_cert:
import asyncio
loop = asyncio.get_running_loop()
self._heweather_cert = HeWeatherCert(root_path=storage_path, loop=loop)
self.hass.data[DOMAIN]['heweather_cert'] = self._heweather_cert
# Get existing public key (don't regenerate)
if self._heweather_cert:
self._jwt_pubkey = await self._heweather_cert.get_pub_key_async()
# Only generate new key if no existing key found
if not self._jwt_pubkey:
await self._heweather_cert.gen_key_async()
self._jwt_pubkey = await self._heweather_cert.get_pub_key_async()
return self.async_show_form(
step_id="auth_jwt_config",
data_schema=self._get_jwt_schema(),
description_placeholders={"jwt_pubkey": self._jwt_pubkey},
)
def _get_jwt_schema(self):
"""Get JWT configuration schema."""
current_jwt_sub = self._config_entry.data.get(CONF_JWT_SUB, "")
current_jwt_kid = self._config_entry.data.get(CONF_JWT_KID, "")
current_host = self._config_entry.data.get(CONF_HOST, DEFAULT_HOST)
return vol.Schema({
vol.Required(
"jwt_sub",
default=current_jwt_sub
): str,
vol.Required(
"jwt_kid",
default=current_jwt_kid
): str,
vol.Required(
"host",
default=current_host
): str
})
async def async_step_location_config(self, user_input: Optional[dict] = None):
"""Handle location configuration."""
if user_input is not None:
longitude = user_input.get("longitude", "")
latitude = user_input.get("latitude", "")
# Validate location inputs
if not longitude or not latitude:
return self.async_show_form(
step_id="location_config",
data_schema=self._get_location_schema(),
errors={"base": "empty_location"}
)
if not validate_longitude(longitude):
return self.async_show_form(
step_id="location_config",
data_schema=self._get_location_schema(),
errors={"base": "invalid_longitude"}
)
if not validate_latitude(latitude):
return self.async_show_form(
step_id="location_config",
data_schema=self._get_location_schema(),
errors={"base": "invalid_latitude"}
)
self._longitude = f"{float(longitude):.4f}"
self._latitude = f"{float(latitude):.4f}"
return await self.async_step_disaster_config()
return self.async_show_form(
step_id="location_config",
data_schema=self._get_location_schema(),
)
def _get_location_schema(self):
"""Get location configuration schema."""
current_longitude = self._config_entry.data.get(CONF_LONGITUDE, "")
current_latitude = self._config_entry.data.get(CONF_LATITUDE, "")
return vol.Schema({
vol.Required(
"longitude",
default=current_longitude
): str,
vol.Required(
"latitude",
default=current_latitude
): str,
})
async def async_step_disaster_config(self, user_input: Optional[dict] = None):
"""Handle disaster configuration."""
if user_input is not None:
self._disasterlevel = user_input.get("disasterlevel", DEFAULT_DISASTER_LEVEL_CONF)
self._disastermsg = user_input.get("disastermsg", DEFAULT_DISASTER_MSG)
return await self.options_flow_done()
return self.async_show_form(
step_id="disaster_config",
data_schema=self._get_disaster_schema(),
)
def _get_disaster_schema(self):
"""Get disaster configuration schema."""
current_disasterlevel = self._config_entry.data.get(CONF_DISASTERLEVEL, DEFAULT_DISASTER_LEVEL_CONF)
current_disastermsg = self._config_entry.data.get(CONF_DISASTERMSG, DEFAULT_DISASTER_MSG)
return vol.Schema({
vol.Required(
"disasterlevel",
default=current_disasterlevel
): vol.In(DISASTER_LEVEL_CONF),
vol.Required(
"disastermsg",
default=current_disastermsg
): vol.In(DISASTER_MSG),
})
async def options_flow_done(self):
"""Create the updated config entry."""
# Get old location data for cleanup
old_longitude = self._config_entry.data.get(CONF_LONGITUDE)
old_latitude = self._config_entry.data.get(CONF_LATITUDE)
# Check if location has changed
location_changed = (
old_longitude != self._longitude or
old_latitude != self._latitude
)
# Build the updated data based on auth method
# Preserve existing auth data when switching methods
updated_data = {
CONF_AUTH_METHOD: self._auth_method,
CONF_STORAGE_PATH: self._config_entry.data.get(CONF_STORAGE_PATH),
CONF_LONGITUDE: self._longitude,
CONF_LATITUDE: self._latitude,
CONF_DISASTERLEVEL: self._disasterlevel,
CONF_DISASTERMSG: self._disastermsg,
}
if self._auth_method == "key":
# Update API key settings, preserve existing JWT settings
updated_data.update({
CONF_KEY: self._key,
CONF_HOST: self._host,
CONF_JWT_SUB: self._config_entry.data.get(CONF_JWT_SUB, ""),
CONF_JWT_KID: self._config_entry.data.get(CONF_JWT_KID, ""),
})
else:
# Update JWT settings, preserve existing API key settings
updated_data.update({
CONF_KEY: self._config_entry.data.get(CONF_KEY, ""),
CONF_HOST: self._host,
CONF_JWT_SUB: self._jwt_sub,
CONF_JWT_KID: self._jwt_kid,
})
# Migrate entities if location changed (更平滑的实体迁移)
if location_changed and old_longitude and old_latitude:
_LOGGER.info(f"Location changed from ({old_longitude}, {old_latitude}) to ({self._longitude}, {self._latitude})")
# 先尝试迁移实体
await migrate_entities_for_location_change(
self.hass, self._config_entry,
old_longitude, old_latitude,
self._longitude, self._latitude
)
# Always check for and clean up duplicate entities
await cleanup_duplicate_entities(self.hass, self._config_entry)
# Update the config entry data directly
self.hass.config_entries.async_update_entry(
self._config_entry, data=updated_data
)
# For options flow, we should reload the entry to apply changes
# 重新加载会创建新实体(如果迁移失败的话)
await self.hass.config_entries.async_reload(self._config_entry.entry_id)
# Return empty result to indicate successful update
return self.async_create_entry(title="", data={})
@@ -0,0 +1,85 @@
DOMAIN: str = 'heweather'
DEFAULT_NAME: str = '和风天气'
# config platform
CONF_AUTH_METHOD = "auth_method"
CONF_OPTIONS = "options"
CONF_LONGITUDE = "longitude"
CONF_LATITUDE = "latitude"
CONF_HOST = "host"
CONF_KEY = "key"
CONF_STORAGE_PATH = "storage_path"
CONF_JWT_SUB = "auth_jwt_sub"
CONF_JWT_KID = "auth_jwt_kid"
DEFAULT_HOST = "devapi.qweather.com"
CONF_DISASTERLEVEL = "disasterlevel"
CONF_DISASTERMSG = "disastermsg"
CONF_SENSOR_LIST = ["air","comf","cw","drsg","flu","sport","trav","uv","sunglass","guomin","liangshai","jiaotong","fangshai","kongtiao","disaster_warn","temprature","humidity","category","feelsLike","text","windDir","windScale","windSpeed","pressure","vis","cloud","dew","precip","qlty","level","primary","pm2p5","pm10","co","so2","no2","o3"]
# config flow
DEFAULT_AUTH_METHOD: str = "key"
AUTH_METHOD: dict = {
"key": "API KEY",
"jwt": "JSON Web Token (Alpha)"
}
DEFAULT_DISASTER_MSG: str = "allmsg"
DISASTER_MSG: dict = {
"title": "仅标题",
"allmsg": "所有信息"
}
DEFAULT_DISASTER_LEVEL_CONF: str = "3"
DISASTER_LEVEL_CONF: dict = {
"1": "标准的",
"2": "次要的",
"3": "中等的",
"4": "主要",
"5": "严重",
"6": "极端"
}
CONDITION_CLASSES = {
'sunny': ["晴"],
'cloudy': ["多云"],
'partlycloudy': ["少云", "晴间多云", "阴"],
'windy': ["有风", "微风", "和风", "清风"],
'windy-variant': ["强风", "劲风", "疾风", "大风", "烈风"],
'hurricane': ["飓风", "龙卷风", "热带风暴", "狂暴风", "风暴"],
'rainy': ["雨", "毛毛雨", "细雨", "小雨", "小到中雨", "中雨", "中到大雨", "大雨", "大到暴雨", "阵雨", "极端降雨", "冻雨"],
'pouring': ["暴雨", "暴雨到大暴雨", "大暴雨", "大暴雨到特大暴雨", "特大暴雨", "强阵雨"],
'lightning-rainy': ["雷阵雨", "强雷阵雨"],
'fog': ["雾", "薄雾", "霾", "浓雾", "强浓雾", "中度霾", "重度霾", "严重霾", "大雾", "特强浓雾"],
'hail': ["雷阵雨伴有冰雹"],
'snowy': ["小雪", "小到中雪", "中雪", "中到大雪", "大雪", "大到暴雪", "暴雪", "阵雪"],
'snowy-rainy': ["雨夹雪", "雨雪天气", "阵雨夹雪"],
'exceptional': ["扬沙", "浮尘", "沙尘暴", "强沙尘暴", "未知"],
}
DISASTER_LEVEL = {
"cancel":0,
"none":0,
"unknown":0,
"standard":1,
"minor":2,
"moderate":3,
"major":4,
"severe":5,
"extreme":6,
"white":0,
"blue":1,
"green":2,
"yellow":3,
"orange":4,
"red":5,
"black":6
}
ATTR_UPDATE_TIME = "更新时间"
ATTR_SUGGESTION = "建议"
ATTRIBUTION = "来自和风天气的天气数据"
CERT_NAME_PREFIX = "heweather_ed25519_"
@@ -0,0 +1,313 @@
import base64
import os
import asyncio
import traceback
from enum import Enum, auto
from pathlib import Path
from typing import Optional, Union
import logging
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
from .const import CERT_NAME_PREFIX
_LOGGER = logging.getLogger(__name__)
class HeWeatherStorageType(Enum):
LOAD = auto()
LOAD_FILE = auto()
SAVE = auto()
SAVE_FILE = auto()
DEL = auto()
DEL_FILE = auto()
CLEAR = auto()
class HeWeatherCert:
_main_loop: asyncio.AbstractEventLoop
_file_future: dict[str, tuple[HeWeatherStorageType, asyncio.Future]]
_root_path: str
_cert_path: str
_openssl_ed25519_private_prefix: bytes
_openssl_ed25519_public_prefix: bytes
_cert_private_name: str
_cert_public_name: str
def __init__(
self, root_path: str, loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._main_loop = loop or asyncio.get_running_loop()
self._file_future = {}
self._root_path = os.path.abspath(root_path)
self._cert_path = os.path.join(self._root_path, "certs")
os.makedirs(self._cert_path, exist_ok=True)
self._openssl_ed25519_private_prefix = bytes.fromhex(
"302e020100300506032b657004220420"
)
self._openssl_ed25519_public_prefix = bytes.fromhex("302a300506032b6570032100")
self._cert_private_name = CERT_NAME_PREFIX + "private.pem"
self._cert_public_name = CERT_NAME_PREFIX + "public.pem"
_LOGGER.debug("cert path, %s", self._cert_path)
def __add_file_future(
self, key: str, op_type: HeWeatherStorageType, fut: asyncio.Future
) -> None:
def fut_done_callback(fut: asyncio.Future):
del fut
self._file_future.pop(key, None)
fut.add_done_callback(fut_done_callback)
self._file_future[key] = op_type, fut
def __save(self, path: str, data: Union[str, None], cover: bool = True) -> bool:
if data is None:
_LOGGER.error("save error, save data is None")
return False
if os.path.exists(path):
if not cover:
_LOGGER.error("save error, file exists, cover is False")
return False
if not os.access(path, os.W_OK):
_LOGGER.error("save error, file not writeable, %s", path)
return False
else:
os.makedirs(os.path.dirname(path), exist_ok=True)
try:
w_bytes: bytes
if isinstance(data, str):
w_bytes = data.encode(encoding="utf-8")
else:
_LOGGER.error(
"save error, unsupported data type, %s", type(data).__name__
)
return False
with open(path, "wb") as w_file:
w_file.write(w_bytes)
return True
except (OSError, TypeError) as e:
_LOGGER.error("save error, %s, %s", e, traceback.format_exc())
return False
async def __save_async(self, path: str, data: Union[str, None]) -> bool:
if path in self._file_future:
# Waiting for the last task to be completed
fut = self._file_future[path][1]
await fut
fut = self._main_loop.run_in_executor(None, self.__save, path, data)
if not fut.done():
self.__add_file_future(path, HeWeatherStorageType.SAVE, fut)
return await fut
def gen_key(self) -> bool:
prikey = ed25519.Ed25519PrivateKey.generate()
pubkey = prikey.public_key()
prikey_bytes = prikey.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption(),
)
pubkey_bytes = pubkey.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
prikey_b64 = base64.b64encode(
self._openssl_ed25519_private_prefix + prikey_bytes
).decode()
pubkey_b64 = base64.b64encode(
self._openssl_ed25519_public_prefix + pubkey_bytes
).decode()
try:
self.__save(
os.path.join(self._cert_path, self._cert_private_name),
f"-----BEGIN PRIVATE KEY-----\n{prikey_b64}\n-----END PRIVATE KEY-----\n",
)
self.__save(
os.path.join(self._cert_path, self._cert_public_name),
f"-----BEGIN PUBLIC KEY-----\n{pubkey_b64}\n-----END PUBLIC KEY-----\n",
)
return True
except Exception as e:
_LOGGER.error("Failed to generate key: %s", e)
self.del_key()
return False
async def gen_key_async(self) -> bool:
prikey = ed25519.Ed25519PrivateKey.generate()
pubkey = prikey.public_key()
prikey_bytes = prikey.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption(),
)
pubkey_bytes = pubkey.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
prikey_b64 = base64.b64encode(
self._openssl_ed25519_private_prefix + prikey_bytes
).decode()
pubkey_b64 = base64.b64encode(
self._openssl_ed25519_public_prefix + pubkey_bytes
).decode()
try:
await self.__save_async(
os.path.join(self._cert_path, self._cert_private_name),
f"-----BEGIN PRIVATE KEY-----\n{prikey_b64}\n-----END PRIVATE KEY-----\n",
)
await self.__save_async(
os.path.join(self._cert_path, self._cert_public_name),
f"-----BEGIN PUBLIC KEY-----\n{pubkey_b64}\n-----END PUBLIC KEY-----\n",
)
return True
except Exception as e:
_LOGGER.error("Failed to generate key: %s", e)
self.del_key()
return False
def __load(self, path: str) -> Union[str, None]:
if not os.path.exists(path):
_LOGGER.debug("load error, file does not exist, %s", path)
return None
if not os.access(path, os.R_OK):
_LOGGER.error("load error, file not readable, %s", path)
return None
try:
with open(path, "rb") as r_file:
r_bytes: bytes = r_file.read()
if r_bytes is None:
_LOGGER.error("load error, empty file, %s", path)
return None
return r_bytes.decode(encoding="utf-8")
except (OSError, TypeError) as e:
_LOGGER.error("load error, %s, %s", e, traceback.format_exc())
return None
async def __load_async(self, path: str) -> Union[str, None]:
if path in self._file_future:
# Waiting for the last task to be completed
op_type, fut = self._file_future[path]
if op_type == HeWeatherStorageType.LOAD:
if not fut.done():
return await fut
else:
await fut
fut = self._main_loop.run_in_executor(None, self.__load, path)
if not fut.done():
self.__add_file_future(path, HeWeatherStorageType.LOAD, fut)
return await fut
def get_pri_key(self) -> Union[str, None]:
try:
return self.__load(os.path.join(self._cert_path, self._cert_private_name))
except Exception as e:
_LOGGER.error("Failed to read private key: %s", e)
return None
async def get_pri_key_async(self) -> Union[str, None]:
try:
return await self.__load_async(
os.path.join(self._cert_path, self._cert_private_name)
)
except Exception as e:
_LOGGER.error("Failed to read private key: %s", e)
return None
def get_pub_key(self) -> Union[str, None]:
try:
return self.__load(os.path.join(self._cert_path, self._cert_public_name))
except Exception as e:
_LOGGER.error("Failed to read public key: %s", e)
return None
async def get_pub_key_async(self) -> Union[str, None]:
try:
return await self.__load_async(
os.path.join(self._cert_path, self._cert_public_name)
)
except Exception as e:
_LOGGER.error("Failed to read public key: %s", e)
return None
def get_jwt_token(self, payload: dict, headers: dict) -> str | None:
private_key = self.get_pri_key()
if private_key:
return jwt.encode(payload, private_key, algorithm="EdDSA", headers=headers)
else:
_LOGGER.error("Failed to get private key")
return None
async def get_jwt_token_async(self, payload: dict, headers: dict) -> str | None:
private_key = await self.get_pri_key_async()
if private_key:
return jwt.encode(payload, private_key, algorithm="EdDSA", headers=headers)
else:
_LOGGER.error("Failed to get private key")
return None
def get_jwt_token_heweather(
self, sub: str, kid: str, iat: int, exp: int
) -> str | None:
payload = {
"iat": iat,
"exp": exp,
"sub": sub,
}
headers = {"kid": kid}
return self.get_jwt_token(payload, headers)
async def get_jwt_token_heweather_async(
self, sub: str, kid: str, iat: int, exp: int
) -> str | None:
payload = {
"iat": iat,
"exp": exp,
"sub": sub,
}
headers = {"kid": kid}
return await self.get_jwt_token_async(payload, headers)
def __remove(self, path: str) -> bool:
item = Path(path)
if item.is_file() or item.is_symlink():
item.unlink()
return True
async def __remove_async(self, path: str) -> bool:
if path in self._file_future:
# Waiting for the last task to be completed
op_type, fut = self._file_future[path]
if op_type == HeWeatherStorageType.DEL:
if not fut.done():
return await fut
else:
await fut
fut = self._main_loop.run_in_executor(None, self.__remove, path)
if not fut.done():
self.__add_file_future(path, HeWeatherStorageType.DEL, fut)
return await fut
def del_key(self) -> None:
self.__remove(os.path.join(self._cert_path, self._cert_private_name))
self.__remove(os.path.join(self._cert_path, self._cert_public_name))
async def del_key_async(self) -> None:
await self.__remove_async(
os.path.join(self._cert_path, self._cert_private_name)
)
await self.__remove_async(os.path.join(self._cert_path, self._cert_public_name))
@@ -0,0 +1,16 @@
{
"domain": "heweather",
"name": "和风天气",
"codeowners": ["@_小愚_"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/c1pher-cn/heweather",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/c1pher-cn/heweather/issues",
"requirements": [
"cryptography",
"PyJWT"
],
"version":"2.4.7"
}
@@ -0,0 +1,893 @@
import logging
from datetime import timedelta
import time
# 此处引入了几个异步处理的库
import asyncio
import async_timeout
import aiohttp
import voluptuous as vol
# aiohttp_client将aiohttp的session与hass关联起来
# track_time_interval需要使用对应的异步的版本
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME,
#TEMP_CELSIUS,
UnitOfTemperature,
PERCENTAGE,
#PRECIPITATION_MILLIMETERS_PER_HOUR,
UnitOfVolumetricFlux,
#SPEED_KILOMETERS_PER_HOUR,
UnitOfSpeed,
#PRESSURE_HPA,
UnitOfPressure,
#LENGTH_KILOMETERS
UnitOfLength
)
from homeassistant.helpers.entity import Entity, DeviceInfo
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from .heweather.const import (
DOMAIN,
CONF_AUTH_METHOD,
CONF_OPTIONS,
CONF_LONGITUDE,
CONF_LATITUDE,
CONF_HOST,
CONF_KEY,
CONF_JWT_SUB,
CONF_JWT_KID,
DEFAULT_HOST,
CONF_DISASTERLEVEL,
CONF_DISASTERMSG,
CONF_SENSOR_LIST,
DISASTER_LEVEL,
ATTR_UPDATE_TIME,
ATTR_SUGGESTION,
ATTRIBUTION
)
_LOGGER = logging.getLogger(__name__)
WEATHER_TIME_BETWEEN_UPDATES = timedelta(seconds=600)
LIFESUGGESTION_TIME_BETWEEN_UPDATES = timedelta(seconds=7200)
OPTIONS = {
"temprature": ["heweather_temperature", "室外温度", "mdi:thermometer", UnitOfTemperature.CELSIUS],
"humidity": ["heweather_humidity", "室外湿度", "mdi:water-percent", PERCENTAGE],
"feelsLike": ["heweather_feelslike", "体感温度", "mdi:thermometer", UnitOfTemperature.CELSIUS],
"text": ["heweather_text", "天气描述", "mdi:thermometer", ' '],
"precip": ["heweather_precip", "小时降水量", "mdi:weather-rainy", UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR],
"windDir": ["heweather_winddir", "风向", "mdi:windsock", ' '],
"windScale": ["heweather_windscale", "风力等级", "mdi:weather-windy", ' '],
"windSpeed": ["heweather_windspeed", "风速", "mdi:weather-windy", UnitOfSpeed.KILOMETERS_PER_HOUR],
"dew": ["heweather_dew", "露点温度", "mdi:thermometer-water", ' '],
"pressure": ["heweather_pressure", "大气压强", "mdi:thermometer", UnitOfPressure.HPA],
"vis": ["heweather_vis", "能见度", "mdi:thermometer", UnitOfLength.KILOMETERS],
"cloud": ["heweather_cloud", "云量", "mdi:cloud-percent", PERCENTAGE],
"primary": ["heweather_primary", "空气质量的主要污染物", "mdi:weather-dust", " "],
"category": ["heweather_category", "空气质量指数级别", "mdi:walk", " "],
"level": ["heweather_level", "空气质量指数等级", "mdi:walk", " "],
"pm2p5": ["heweather_pm25", "PM2.5", "mdi:walk", " "],
"pm10": ["heweather_pm10", "PM10", "mdi:walk", " "],
"no2": ["heweather_no2", "二氧化氮", "mdi:emoticon-dead", " "],
"so2": ["heweather_so2", "二氧化硫", "mdi:emoticon-dead", " "],
"co": ["heweather_co", "一氧化碳", "mdi:molecule-co", " "],
"o3": ["heweather_o3", "臭氧", "mdi:weather-cloudy", " "],
"no": ["heweather_no", "一氧化氮", "mdi:emoticon-dead", " "],
"nmhc": ["heweather_nmhc", "非甲烷总烃", "mdi:emoticon-dead", " "],
"qlty": ["heweather_qlty", "综合空气质量", "mdi:quality-high", " "],
"disaster_warn": ["heweather_disaster_warn", "灾害预警", "mdi:alert", " "],
"air": ["suggestion_air", "空气污染扩散条件指数", "mdi:air-conditioner", " "],
"comf": ["suggestion_comf", "舒适度指数", "mdi:human-greeting", " "],
"cw": ["suggestion_cw", "洗车指数", "mdi:car", " "],
"drsg": ["suggestion_drsg", "穿衣指数", "mdi:hanger", " "],
"flu": ["suggestion_flu", "感冒指数", "mdi:biohazard", " "],
"sport": ["suggestion_sport", "运动指数", "mdi:badminton", " "],
"trav": ["suggestion_trav", "旅行指数", "mdi:wallet-travel", " "],
"uv": ["suggestion_uv", "紫外线指数", "mdi:sun-wireless", " "],
"guomin": ["suggestion_guomin", "过敏指数", "mdi:sunglasses", " "],
"kongtiao": ["suggestion_kongtiao", "空调开启指数", "mdi:air-conditioner", " "],
"sunglass": ["suggestion_sunglass", "太阳镜指数", "mdi:sunglasses", " "],
"fangshai": ["suggestion_fangshai", "防晒指数", "mdi:shield-sun-outline", " "],
"liangshai": ["suggestion_liangshai", "晾晒指数", "mdi:tshirt-crew-outline", " "],
"jiaotong": ["suggestion_jiaotong", "交通指数", "mdi:train-car", " "],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_LONGITUDE): cv.string,
vol.Required(CONF_LATITUDE): cv.string,
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Required(CONF_KEY): cv.string,
vol.Required(CONF_DISASTERLEVEL): cv.string,
vol.Required(CONF_DISASTERMSG): cv.string,
})
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
"""这个协程是程序的入口,其中add_devices函数也变成了异步版本."""
_LOGGER.info("setup platform sensor.Heweather...")
longitude = config_entry.data.get(CONF_LONGITUDE)
latitude = config_entry.data.get(CONF_LATITUDE)
host = config_entry.data.get(CONF_HOST)
key = config_entry.data.get(CONF_KEY)
disastermsg = config_entry.data.get(CONF_DISASTERMSG)
disasterlevel = config_entry.data.get(CONF_DISASTERLEVEL)
# 这里通过 data 实例化class weatherdata,并传入调用API所需信息
auth_method = config_entry.data.get(CONF_AUTH_METHOD)
if auth_method == "key":
key = config_entry.data.get(CONF_KEY)
suggestion_data = SuggestionData(hass, longitude, latitude, host, key=key)
weather_data = WeatherData(hass, longitude, latitude, host, disastermsg, disasterlevel, key=key)
else:
# HeWeather Certification
heweather_cert = hass.data[DOMAIN].get('heweather_cert', None)
jwt_sub = config_entry.data.get(CONF_JWT_SUB)
jwt_kid = config_entry.data.get(CONF_JWT_KID)
suggestion_data = SuggestionData(hass, longitude, latitude, host, heweather_cert=heweather_cert, jwt_sub=jwt_sub, jwt_kid=jwt_kid)
weather_data = WeatherData(hass, longitude, latitude, host, disastermsg, disasterlevel, heweather_cert=heweather_cert, jwt_sub=jwt_sub, jwt_kid=jwt_kid)
await weather_data.async_update(dt_util.now())
config_entry.async_on_unload(async_track_time_interval(hass, weather_data.async_update, WEATHER_TIME_BETWEEN_UPDATES, cancel_on_shutdown=True))
await suggestion_data.async_update(dt_util.now())
config_entry.async_on_unload(async_track_time_interval(hass, suggestion_data.async_update, LIFESUGGESTION_TIME_BETWEEN_UPDATES, cancel_on_shutdown=True))
dev = []
for option in CONF_SENSOR_LIST:
dev.append(HeweatherWeatherSensor(weather_data, suggestion_data, option, longitude, latitude))
async_add_entities(dev, True)
#@asyncio.coroutine
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""这个协程是程序的入口,其中add_devices函数也变成了异步版本."""
_LOGGER.info("setup platform sensor.Heweather...")
longitude = config.get(CONF_LONGITUDE)
latitude = config.get(CONF_LATITUDE)
host = config.get(CONF_HOST)
key = config.get(CONF_KEY)
disastermsg = config.get(CONF_DISASTERMSG)
disasterlevel = config.get(CONF_DISASTERLEVEL)
# 这里通过 data 实例化class weatherdata,并传入调用API所需信息
weather_data = WeatherData(hass, longitude, latitude, host, disastermsg, disasterlevel, key=key)
suggestion_data = SuggestionData(hass, longitude, latitude, host, key=key)
await weather_data.async_update(dt_util.now())
async_track_time_interval(hass, weather_data.async_update, WEATHER_TIME_BETWEEN_UPDATES, cancel_on_shutdown=True)
await suggestion_data.async_update(dt_util.now())
async_track_time_interval(hass, suggestion_data.async_update, LIFESUGGESTION_TIME_BETWEEN_UPDATES, cancel_on_shutdown=True)
dev = []
for option in CONF_SENSOR_LIST:
dev.append(HeweatherWeatherSensor(weather_data, suggestion_data, option, longitude, latitude))
async_add_devices(dev, True)
class HeweatherWeatherSensor(Entity):
"""定义一个温度传感器的类,继承自HomeAssistant的Entity类."""
_attr_has_entity_name = True
def __init__(self, weather_data, suggestion_data, option, longitude, latitude):
"""初始化."""
self._weather_data = weather_data
self._suggestion_data = suggestion_data
self._object_id = OPTIONS[option][0]
# 【修改重点】直接使用 OPTIONS 里的中文名称作为实体名
# OPTIONS[option][1] 对应代码最上面的字典里的 "舒适度指数"、"洗车指数" 等中文
self._attr_name = OPTIONS[option][0]
self._icon = OPTIONS[option][2]
self._unit_of_measurement = OPTIONS[option][3]
self._type = option
self._state = None
self._attributes = {"states":"null"}
self._updatetime = None
self._attr_unique_id = f"{OPTIONS[option][0]}_{longitude}_{latitude}"
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
identifiers={(DOMAIN, f"{self._weather_data._params['location']}")},
name="和风天气",
manufacturer="QWeather",
model="API v7",
entry_type=None,
)
@property
def extra_state_attributes(self):
"""Return entity specific state attributes."""
return self._attributes
@property
def translation_key(self):
"""Return the translation key to translate the entity's name and states."""
return self._object_id
#@property
#def name(self):
# """返回实体的名字."""
# return self._name
#@property
#def registry_name(self):
# """返回实体的friendly_name属性."""
# return self._friendly_name
@property
def state(self):
"""返回当前的状态."""
return self._state
@property
def icon(self):
"""返回icon属性."""
return self._icon
@property
def unit_of_measurement(self):
"""返回unit_of_measuremeng属性."""
return self._unit_of_measurement
@property
def device_state_attributes(self):
"""设置其它一些属性值."""
if self._state is not None:
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
ATTR_UPDATE_TIME: self._updatetime
}
#@asyncio.coroutine
async def async_update(self):
"""update函数变成了async_update."""
self._updatetime = self._weather_data.updatetime
if self._type == "temprature":
self._state = self._weather_data.temprature
elif self._type == "humidity":
self._state = self._weather_data.humidity
elif self._type == "feelsLike":
self._state = self._weather_data.feelsLike
elif self._type == "text":
self._state = self._weather_data.text
elif self._type == "windDir":
self._state = self._weather_data.windDir
elif self._type == "windScale":
self._state = self._weather_data.windScale
elif self._type == "windSpeed":
self._state = self._weather_data.windSpeed
elif self._type == "precip":
self._state = self._weather_data.precip
elif self._type == "pressure":
self._state = self._weather_data.pressure
elif self._type == "vis":
self._state = self._weather_data.vis
elif self._type == "dew":
self._state = self._weather_data.dew
elif self._type == "cloud":
self._state = self._weather_data.cloud
elif self._type == "category":
self._state = self._weather_data.category
elif self._type == "primary":
self._state = self._weather_data.primary
elif self._type == "level":
self._state = self._weather_data.level
elif self._type == "pm10":
self._state = self._weather_data.pm10
elif self._type == "pm2p5":
self._state = self._weather_data.pm2p5
elif self._type == "no2":
self._state = self._weather_data.no2
elif self._type == "so2":
self._state = self._weather_data.so2
elif self._type == "co":
self._state = self._weather_data.co
elif self._type == "o3":
self._state = self._weather_data.o3
elif self._type == "no":
self._state = self._weather_data.no
elif self._type == "nmhc":
self._state = self._weather_data.nmhc
elif self._type == "qlty":
self._state = self._weather_data.qlty
elif self._type == "disaster_warn":
if len(self._weather_data.disaster_warn) > 10:
self._state = 'on'
self._attributes["states"] = self._weather_data.disaster_warn
else:
self._state = 'off'
self._attributes["states"] = self._weather_data.disaster_warn
#lifesuggestion
elif self._type == "air":
self._state = self._suggestion_data.air[0]
self._attributes["states"] = self._suggestion_data.air[1]
elif self._type == "comf":
self._state = self._suggestion_data.comf[0]
self._attributes["states"] = self._suggestion_data.comf[1]
elif self._type == "cw":
self._state = self._suggestion_data.cw[0]
self._attributes["states"] = self._suggestion_data.cw[1]
elif self._type == "drsg":
self._state = self._suggestion_data.drsg[0]
self._attributes["states"] = self._suggestion_data.drsg[1]
elif self._type == "flu":
self._state = self._suggestion_data.flu[0]
self._attributes["states"] = self._suggestion_data.flu[1]
elif self._type == "sport":
self._state = self._suggestion_data.sport[0]
self._attributes["states"] = self._suggestion_data.sport[1]
elif self._type == "trav":
self._state = self._suggestion_data.trav[0]
self._attributes["states"] = self._suggestion_data.trav[1]
elif self._type == "uv":
self._state = self._suggestion_data.uv[0]
self._attributes["states"] = self._suggestion_data.uv[1]
elif self._type == "guomin":
self._state = self._suggestion_data.guomin[0]
self._attributes["states"] = self._suggestion_data.guomin[1]
elif self._type == "kongtiao":
self._state = self._suggestion_data.kongtiao[0]
self._attributes["states"] = self._suggestion_data.kongtiao[1]
elif self._type == "liangshai":
self._state = self._suggestion_data.liangshai[0]
self._attributes["states"] = self._suggestion_data.liangshai[1]
elif self._type == "fangshai":
self._state = self._suggestion_data.fangshai[0]
self._attributes["states"] = self._suggestion_data.fangshai[1]
elif self._type == "sunglass":
self._state = self._suggestion_data.uv[0]
self._attributes["states"] = self._suggestion_data.uv[1]
elif self._type == "jiaotong":
self._state = self._suggestion_data.jiaotong[0]
self._attributes["states"] = self._suggestion_data.jiaotong[1]
# 设置污染物单位
pollutant_types = {"pm10", "pm2p5", "no2", "so2", "co", "o3", "no", "nmhc"}
if self._type in pollutant_types:
unit = getattr(self._weather_data, f"{self._type}_unit", None)
if unit:
self._unit_of_measurement = unit
class WeatherData(object):
"""天气相关的数据,存储在这个类中."""
def __init__(self, hass, longitude, latitude, host, disastermsg, disasterlevel, key=None, heweather_cert=None, jwt_sub=None, jwt_kid=None):
"""初始化函数."""
self._hass = hass
location = f"{longitude},{latitude}"
self._disastermsg = disastermsg
self._disasterlevel = disasterlevel
#disastermsg, disasterlevel
self._temprature = None
self._humidity = None
if key is not None:
self._is_jwt = False
self._weather_now_url = "https://"+host+"/v7/weather/now?location="+location+"&key="+key
self._air_now_url = "https://"+host+"/airquality/v1/current/"+latitude+"/"+longitude+"?key="+key
self._disaster_warn_url = "https://"+host+"/weatheralert/v1/current/"+latitude+"/"+longitude+"?key="+key
self._params = {"location": location,
"key": key}
else:
self._is_jwt = True
self._weather_now_url = "https://"+host+"/v7/weather/now?location="+location
self._air_now_url = "https://"+host+"/airquality/v1/current/"+latitude+"/"+longitude
self._disaster_warn_url = "https://"+host+"/weatheralert/v1/current/"+latitude+"/"+longitude
self._params = {"location": location}
self._heweather_cert = heweather_cert
self._jwt_sub = jwt_sub
self._jwt_kid = jwt_kid
self._feelsLike = None
self._text = None
self._windDir = None
self._windScale = None
self._windSpeed = None
self._precip = None
self._pressure = None
self._vis = None
self._cloud = None
self._dew = None
self._updatetime = None
self._category = None
self._pm10 = None
self._primary = None
self._level = None
self._pm2p5 = None
self._no2 = None
self._so2 = None
self._co = None
self._o3 = None
self._no = None
self._nmhc = None
# 新 API 单位不固定,动态获取
self._pm10_unit = None
self._pm2p5_unit = None
self._no2_unit = None
self._so2_unit = None
self._co_unit = None
self._o3_unit = None
self._no_unit = None
self._nmhc_unit = None
self._qlty = None
self._disaster_warn = None
self._updatetime = None
@property
def temprature(self):
"""温度."""
return self._temprature
@property
def humidity(self):
"""湿度."""
return self._humidity
@property
def feelsLike(self):
"""体感温度"""
return self._feelsLike
@property
def text(self):
"""天气状况的文字描述,包括阴晴雨雪等天气状态的描述"""
return self._text
@property
def windDir(self):
"""风向"""
return self._windDir
@property
def category(self):
"""空气质量指数级别"""
return self._category
@property
def level(self):
"""空气质量指数等级"""
return self._level
@property
def primary(self):
"""空气质量的主要污染物,空气质量为优时,返回值为NA"""
return self._primary
@property
def windScale(self):
"""风力等级"""
return self._windScale
@property
def windSpeed(self):
"""风速,公里/小时"""
return self._windSpeed
@property
def precip(self):
"""当前小时累计降水量,默认单位:毫米"""
return self._precip
@property
def pressure(self):
"""大气压强,默认单位:百帕"""
return self._pressure
@property
def vis(self):
"""能见度,默认单位:公里"""
return self._vis
@property
def cloud(self):
"""云量,百分比数值。可能为空"""
return self._cloud
@property
def dew(self):
"""露点温度。可能为空"""
return self._dew
@property
def pm2p5(self):
"""pm2.5"""
return self._pm2p5
@property
def pm2p5_unit(self):
return self._pm2p5_unit
@property
def pm10(self):
"""pm10"""
return self._pm10
@property
def pm10_unit(self):
return self._pm10_unit
@property
def qlty(self):
"""(aqi)空气质量指数"""
return self._qlty
@property
def no2(self):
"""no2"""
return self._no2
@property
def no2_unit(self):
return self._no2_unit
@property
def co(self):
"""co"""
return self._co
@property
def co_unit(self):
return self._co_unit
@property
def so2(self):
"""so2"""
return self._so2
@property
def so2_unit(self):
return self._so2_unit
@property
def o3(self):
"""o3"""
return self._o3
@property
def o3_unit(self):
return self._o3_unit
@property
def no(self):
"""no"""
return self._no
@property
def no_unit(self):
return self._no_unit
@property
def nmhc(self):
"""nmhc"""
return self._nmhc
@property
def nmhc_unit(self):
return self._nmhc_unit
@property
def disaster_warn(self):
"""灾害预警"""
return self._disaster_warn
@property
def updatetime(self):
"""更新时间."""
return self._updatetime
#@asyncio.coroutine
async def async_update(self, now):
"""从远程更新信息."""
_LOGGER.info("Update from JingdongWangxiang's OpenAPI...")
# 通过HTTP访问,获取需要的信息
# 此处使用了基于aiohttp库的async_get_clientsession
try:
timeout = aiohttp.ClientTimeout(total=20)
connector = aiohttp.TCPConnector(limit=10)
headers = None
if self._is_jwt:
jwt_token = await self._heweather_cert.get_jwt_token_heweather_async(self._jwt_sub, self._jwt_kid, int(time.time()) - 30, int(time.time()) + 180)
headers = {'Authorization': f'Bearer {jwt_token}'}
async with aiohttp.ClientSession(connector=connector, timeout=timeout, headers=headers) as session:
async with session.get(self._weather_now_url) as response:
json_data = await response.json()
weather = json_data["now"]
async with session.get(self._air_now_url) as response:
json_data = await response.json()
# AQI (CN) code: cn-mee
air_index = next((item for item in json_data["indexes"] if item["code"] == "cn-mee"), None)
air_pollutants = json_data["pollutants"]
async with session.get(self._disaster_warn_url) as response:
json_data = await response.json()
disaster_warn = json_data["alerts"]
except(asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error while accessing: %s", self._weather_now_url)
return
# 根据http返回的结果,更新数据
self._temprature = weather["temp"]
self._humidity = weather["humidity"]
self._feelsLike = weather["feelsLike"]
self._text = weather["text"]
self._windDir = weather["windDir"]
self._windScale = weather["windScale"]
self._windSpeed = weather["windSpeed"]
self._precip = weather["precip"]
self._pressure = weather["pressure"]
self._vis = weather["vis"]
self._cloud = weather["cloud"]
self._dew = weather["dew"]
self._updatetime = weather["obsTime"]
self._qlty = air_index["aqiDisplay"]
self._level = air_index["level"]
self._category = air_index["category"]
primary_pollutant = air_index["primaryPollutant"]
if primary_pollutant is not None and "name" in primary_pollutant:
self._primary = primary_pollutant["name"]
supported_codes = {"pm10", "pm2p5", "co", "no", "no2", "so2", "o3", "nmhc"}
for pollutant in air_pollutants:
code = pollutant["code"]
if code in supported_codes:
setattr(self, f"_{code}", pollutant["concentration"]["value"])
setattr(self, f"_{code}_unit", pollutant["concentration"]["unit"])
allmsg=''
titlemsg=''
# Normalize disaster_warn into a list for safe iteration
if disaster_warn is None:
alerts = []
elif isinstance(disaster_warn, dict):
alerts = [disaster_warn]
elif isinstance(disaster_warn, list):
alerts = disaster_warn
else:
# Unexpected type: try to coerce to list if possible, else empty
try:
alerts = list(disaster_warn)
except Exception:
alerts = []
for i in alerts:
#if DISASTER_LEVEL[i["severity"]] >= 订阅等级:
severity = i.get("severity", "").lower()
if severity in DISASTER_LEVEL and (DISASTER_LEVEL[severity] >= int(self._disasterlevel)):
allmsg = allmsg +i["headline"] + ':' + i["description"] + '||'
titlemsg = titlemsg + i["headline"] + '||'
if(len(titlemsg)<5):
self._disaster_warn = '近日无'+ self._disasterlevel +'级及以上灾害'
#if(订阅标题)
elif(self._disastermsg=='title'):
self._disaster_warn = titlemsg
else:
self._disaster_warn = allmsg
class SuggestionData(object):
"""天气相关建议的数据,存储在这个类中."""
def __init__(self, hass, longitude, latitude, host, key=None, heweather_cert=None, jwt_sub=None, jwt_kid=None):
"""初始化函数."""
self._hass = hass
location = f"{longitude},{latitude}"
if key is not None:
self._url = "https://"+host+"/v7/indices/1d?location="+location+"&key="+key+"&type=0"
self._params = {"location": location,
"key": key,
"type": 0
}
self._is_jwt = False
else:
self._url = "https://"+host+"/v7/indices/1d?location="+location+"&type=0"
self._params = {"location": location,
"type": 0
}
self._is_jwt = True
self._heweather_cert = heweather_cert
self._jwt_sub = jwt_sub
self._jwt_kid = jwt_kid
self._updatetime = ["1","1"]
self._air = ["1","1"]
self._comf = ["1","1"]
self._cw = ["1","1"]
self._drsg = ["1","1"]
self._flu = ["1","1"]
self._sport = ["1","1"]
self._trav = ["1","1"]
self._uv = ["1","1"]
self._guomin = None
self._kongtiao = None
self._sunglass = None
self._liangshai = None
self._fangshai = None
self._jiaotong = None
@property
def updatetime(self):
"""更新时间."""
return self._updatetime
@property
def air(self):
"""空气污染扩散条件指数"""
return self._air
@property
def comf(self):
"""舒适度指数"""
return self._comf
@property
def cw(self):
"""洗车建议"""
return self._cw
@property
def drsg(self):
"""穿着建议"""
return self._drsg
@property
def flu(self):
"""流感提示"""
return self._flu
@property
def sport(self):
"""运动建议"""
return self._sport
@property
def trav(self):
"""旅游指南"""
return self._trav
@property
def uv(self):
"""紫外线"""
return self._uv
@property
def guomin(self):
"""过敏指数"""
return self._guomin
@property
def kongtiao(self):
"""空调指数"""
return self._kongtiao
@property
def sunglass(self):
"""太阳镜指数"""
return self._sunglass
@property
def liangshai(self):
"""晾晒指数"""
return self._liangshai
@property
def fangshai(self):
"""防晒指数"""
return self._fangshai
@property
def jiaotong(self):
"""交通指数"""
return self._jiaotong
#@asyncio.coroutine
async def async_update(self, now):
"""从远程更新信息."""
try:
session = async_get_clientsession(self._hass)
headers = None
if self._is_jwt:
jwt_token = await self._heweather_cert.get_jwt_token_heweather_async(self._jwt_sub, self._jwt_kid, int(time.time()) - 30, int(time.time()) + 180)
headers = {'Authorization': f'Bearer {jwt_token}'}
with async_timeout.timeout(15):
#with async_timeout.timeout(15, loop=self._hass.loop):
response = await session.get(
self._url, headers=headers)
except(asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error while accessing: %s", self._url)
return
if response.status != 200:
_LOGGER.error("Error while accessing: %s, status=%d",
self._url,
response.status)
return
result = await response.json()
if result is None:
_LOGGER.error("Request api Error")
return
elif result["code"] != "200":
_LOGGER.error("Error API return, code=%s,url=%s",
result["code"],self._url)
return
# 根据http返回的结果,更新数据
all_result = result["daily"]
self._updatetime = result["updateTime"]
for i in all_result:
if i["type"] == "1":
self._sport = [i["category"], i["text"]]
if i["type"] == "10":
self._air = [i["category"], i["text"]]
if i["type"] == "8":
self._comf = [i["category"], i["text"]]
if i["type"] == "2":
self._cw = [i["category"], i["text"]]
if i["type"] == "3":
self._drsg = [i["category"], i["text"]]
if i["type"] == "9":
self._flu = [i["category"], i["text"]]
if i["type"] == "6":
self._trav = [i["category"], i["text"]]
if i["type"] == "5":
self._uv = [i["category"], i["text"]]
if i["type"] == "7":
self._guomin = [i["category"], i["text"]]
if i["type"] == "11":
self._kongtiao = [i["category"], i["text"]]
if i["type"] == "12":
self._sunglass = [i["category"], i["text"]]
if i["type"] == "14":
self._liangshai = [i["category"], i["text"]]
if i["type"] == "15":
self._jiaotong = [i["category"], i["text"]]
if i["type"] == "16":
self._fangshai = [i["category"], i["text"]]
@@ -0,0 +1,246 @@
{
"entity": {
"sensor": {
"heweather_temperature": {
"name": "outside temperature"
},
"heweather_humidity": {
"name": "outside humidity"
},
"heweather_feelslike": {
"name": "feelslike temperature"
},
"heweather_text": {
"name": "weather text"
},
"heweather_precip": {
"name": "precip"
},
"heweather_winddir": {
"name": "winddir"
},
"heweather_windscale": {
"name": "windscale"
},
"heweather_windspeed": {
"name": "windspeed"
},
"heweather_dew": {
"name": "dew temperature"
},
"heweather_pressure": {
"name": "pressure"
},
"heweather_vis": {
"name": "vis"
},
"heweather_cloud": {
"name": "cloud"
},
"heweather_primary": {
"name": "primary"
},
"heweather_category": {
"name": "category"
},
"heweather_level": {
"name": "category level"
},
"heweather_pm25": {
"name": "PM2.5"
},
"heweather_pm10": {
"name": "PM10"
},
"heweather_no2": {
"name": "no2"
},
"heweather_so2": {
"name": "so2"
},
"heweather_co": {
"name": "co"
},
"heweather_o3": {
"name": "o3"
},
"heweather_no": {
"name": "no"
},
"heweather_nmhc": {
"name": "nmhc"
},
"heweather_qlty": {
"name": "qlty"
},
"heweather_disaster_warn": {
"name": "disaster_warn"
},
"suggestion_air": {
"name": "Air Pollution Dispersion Index"
},
"suggestion_comf": {
"name": "Comfort Index"
},
"suggestion_cw": {
"name": "Car Washing Index"
},
"suggestion_drsg": {
"name": "Dressing Index"
},
"suggestion_flu": {
"name": "Cold/Flu Index"
},
"suggestion_sport": {
"name": "Outdoor Sports Index"
},
"suggestion_trav": {
"name": "Travel Index"
},
"suggestion_uv": {
"name": "UV Index"
},
"suggestion_guomin": {
"name": "Allergy Index"
},
"suggestion_kongtiao": {
"name": "Air Conditioning Usage Index"
},
"suggestion_sunglass": {
"name": "Sunglasses Index"
},
"suggestion_fangshai": {
"name": "Sun Protection Index"
},
"suggestion_liangshai": {
"name": "Clothes Drying Index"
},
"suggestion_jiaotong": {
"name": "Traffic Condition Index"
}
}
},
"config": {
"abort": {
"single_instance_allowed": "single instance allowed。"
},
"error": {
"empty_location": "empty location",
"invalid_longitude": "invalid_longitude",
"invalid_latitude": "invalid_latitude",
"key is empty": "key is empty",
"host is empty": "host is empty",
"jwt_sub is empty": "jwt_sub is empty",
"jwt_kid is empty": "jwt_kid is empty"
},
"flow_title": "heweather",
"step": {
"auth_method_config": {
"title": "Select your auth method",
"description": "Select your auth method. Note: According to the official QWeather notice, starting January 1, 2027, the number of requests using API Key authentication will be subject to limits.",
"data": {
"auth_method": "auth method"
}
},
"auth_apikey_config": {
"title": "API KEY config",
"description": "Please enter the QWeather API Key and API Host. Note: According to the official QWeather notice, starting January 1, 2027, the number of requests using API Key authentication will be subject to limits.",
"data": {
"key": "API KEY",
"host": "API Host"
}
},
"auth_jwt_config": {
"title": "JWT config",
"description": "Please upload the public key\n```\n{jwt_pubkey}\n```\n to the QWeather developer console, then enter the corresponding Project ID, Credential ID, and API Host.",
"data": {
"jwt_sub": "Project ID",
"jwt_kid": "Credential ID",
"host": "API Host"
}
},
"location_config": {
"title": "location config",
"description": "Please enter longitude and latitude coordinates. Longitude range: -180 to 180, latitude range: -90 to 90. Coordinates will be rounded to 4 decimal places.",
"data": {
"longitude": "longitude",
"latitude": "latitude"
}
},
"disaster_config": {
"title": "disaster config",
"description": "please update the disaster config of heweather",
"data": {
"disasterlevel": "disaster level",
"disastermsg": "disaster type"
}
}
}
},
"options": {
"error": {
"empty_location": "location is empty",
"invalid_longitude": "invalid longitude",
"invalid_latitude": "invalid latitude",
"key is empty": "key is empty",
"host is empty": "host is empty",
"jwt_sub is empty": "jwt_sub is empty",
"jwt_kid is empty": "jwt_kid is empty"
},
"step": {
"auth_method_config": {
"title": "Select your auth method",
"description": "Select your auth method. Note: According to the official QWeather notice, starting January 1, 2027, the number of requests using API Key authentication will be subject to limits.",
"data": {
"auth_method": "auth method"
}
},
"auth_apikey_config": {
"title": "Update API KEY",
"description": "Please enter the QWeather API Key and API Host. Note: According to the official QWeather notice, starting January 1, 2027, the number of requests using API Key authentication will be subject to limits.",
"data": {
"key": "API KEY",
"host": "API Host"
}
},
"auth_jwt_config": {
"title": "Update JWT config",
"description": "Please upload the public key\n```\n{jwt_pubkey}\n```\n to the QWeather developer console, then enter the corresponding Project ID, Credential ID, and API Host.",
"data": {
"jwt_sub": "Project ID",
"jwt_kid": "Credential ID",
"host": "API Host"
}
},
"location_config": {
"title": "Update your location",
"description": "Please enter longitude and latitude coordinates. Longitude range: -180 to 180, latitude range: -90 to 90. Coordinates will be rounded to 4 decimal places.",
"data": {
"longitude": "longitude",
"latitude": "latitude"
}
},
"disaster_config": {
"title": "update disaster config",
"description": "please update the disaster config of heweather",
"data": {
"disasterlevel": "disaster level",
"disastermsg": "disaster type"
}
},
"user": {
"title": "update config",
"description": "please update the config of heweather",
"data": {
"longitude": "longitude",
"latitude": "latitude",
"disasterlevel": "disaster level",
"disastermsg": "disaster type"
}
}
}
}
}
@@ -0,0 +1,246 @@
{
"entity": {
"sensor": {
"heweather_temperature": {
"name": "室外温度"
},
"heweather_humidity": {
"name": "室外湿度"
},
"heweather_feelslike": {
"name": "体感温度"
},
"heweather_text": {
"name": "天气描述"
},
"heweather_precip": {
"name": "小时降水量"
},
"heweather_winddir": {
"name": "风向"
},
"heweather_windscale": {
"name": "风力等级"
},
"heweather_windspeed": {
"name": "风速"
},
"heweather_dew": {
"name": "露点温度"
},
"heweather_pressure": {
"name": "大气压强"
},
"heweather_vis": {
"name": "能见度"
},
"heweather_cloud": {
"name": "云量"
},
"heweather_primary": {
"name": "空气质量主要污染物"
},
"heweather_category": {
"name": "空气质量指数级别"
},
"heweather_level": {
"name": "空气质量指数等级"
},
"heweather_pm25": {
"name": "PM2.5"
},
"heweather_pm10": {
"name": "PM10"
},
"heweather_no2": {
"name": "二氧化氮"
},
"heweather_so2": {
"name": "二氧化硫"
},
"heweather_co": {
"name": "一氧化碳"
},
"heweather_o3": {
"name": "臭氧"
},
"heweather_no": {
"name": "一氧化氮"
},
"heweather_nmhc": {
"name": "非甲烷总烃"
},
"heweather_qlty": {
"name": "综合空气质量"
},
"heweather_disaster_warn": {
"name": "灾害预警"
},
"suggestion_air": {
"name": "空气污染扩散条件指数"
},
"suggestion_comf": {
"name": "舒适度指数"
},
"suggestion_cw": {
"name": "洗车指数"
},
"suggestion_drsg": {
"name": "穿衣指数"
},
"suggestion_flu": {
"name": "感冒指数"
},
"suggestion_sport": {
"name": "运动指数"
},
"suggestion_trav": {
"name": "旅行指数"
},
"suggestion_uv": {
"name": "紫外线指数"
},
"suggestion_guomin": {
"name": "过敏指数"
},
"suggestion_kongtiao": {
"name": "空调开启指数"
},
"suggestion_sunglass": {
"name": "太阳镜指数"
},
"suggestion_fangshai": {
"name": "防晒指数"
},
"suggestion_liangshai": {
"name": "晾晒指数"
},
"suggestion_jiaotong": {
"name": "交通指数"
}
}
},
"config": {
"abort": {
"single_instance_allowed": "只允许配置一个和风天气集成条目。"
},
"error": {
"empty_location": "经度和纬度不能为空",
"invalid_longitude": "经度格式不正确,必须是-180到180之间的数字",
"invalid_latitude": "纬度格式不正确,必须是-90到90之间的数字",
"key is empty": "API KEY不能为空",
"host is empty": "API Host不能为空",
"jwt_sub is empty": "项目ID不能为空",
"jwt_kid is empty": "凭据ID不能为空"
},
"flow_title": "和风天气",
"step": {
"auth_method_config": {
"title": "身份认证方式",
"description": "请选择使用的身份认证方式。\r\n**注意:根据和风天气官方说明,从2027年1月1日起,使用API KEY认证方式都将受请求量的限制。**",
"data": {
"auth_method": "认证方式"
}
},
"auth_apikey_config": {
"title": "API KEY认证",
"description": "请输入和风天气的API KEY和API Host。\r\n**注意:根据和风天气官方说明,从2027年1月1日起,使用API KEY认证方式都将受请求量的限制。**",
"data": {
"key": "API KEY",
"host": "API Host"
}
},
"auth_jwt_config": {
"title": "JWT认证",
"description": "请将公钥\n```\n{jwt_pubkey}\n```\n上传至和风天气开发控制台,并输入对应的项目ID、凭据ID和API Host。",
"data": {
"jwt_sub": "项目ID",
"jwt_kid": "凭据ID",
"host": "API Host"
}
},
"location_config": {
"title": "位置设置",
"description": "请输入经纬度坐标。经度范围:-180到180,纬度范围:-90到90。",
"data": {
"longitude": "经度",
"latitude": "纬度"
}
},
"disaster_config": {
"title": "灾害预警",
"description": "请设置灾害预警的相关参数。",
"data": {
"disasterlevel": "预警级别",
"disastermsg": "预警类型"
}
}
}
},
"options": {
"error": {
"empty_location": "经度和纬度不能为空",
"invalid_longitude": "经度格式不正确,必须是-180到180之间的数字",
"invalid_latitude": "纬度格式不正确,必须是-90到90之间的数字",
"key is empty": "API KEY不能为空",
"host is empty": "API Host不能为空",
"jwt_sub is empty": "项目ID不能为空",
"jwt_kid is empty": "凭据ID不能为空"
},
"step": {
"auth_method_config": {
"title": "更新身份认证方式",
"description": "请选择使用的身份认证方式。\r\n**注意:根据和风天气官方说明,从2027年1月1日起,使用API KEY认证方式都将受请求量的限制。**",
"data": {
"auth_method": "认证方式"
}
},
"auth_apikey_config": {
"title": "更新API KEY认证",
"description": "请输入和风天气的API KEY和API Host。\r\n**注意:根据和风天气官方说明,从2027年1月1日起,使用API KEY认证方式都将受请求量的限制。**",
"data": {
"key": "API KEY",
"host": "API Host"
}
},
"auth_jwt_config": {
"title": "更新JWT认证",
"description": "请将公钥\n```\n{jwt_pubkey}\n```\n上传至和风天气开发控制台,并输入对应的项目ID、凭据ID和API Host。",
"data": {
"jwt_sub": "项目ID",
"jwt_kid": "凭据ID",
"host": "API Host"
}
},
"location_config": {
"title": "更新位置设置",
"description": "请输入经纬度坐标。经度范围:-180到180,纬度范围:-90到90。坐标将保留4位小数精度。",
"data": {
"longitude": "经度",
"latitude": "纬度"
}
},
"disaster_config": {
"title": "更新灾害预警",
"description": "请设置灾害预警的相关参数。",
"data": {
"disasterlevel": "预警级别",
"disastermsg": "预警类型"
}
},
"user": {
"title": "配置更新",
"description": "请更新和风天气集成的配置。",
"data": {
"longitude": "经度",
"latitude": "纬度",
"disasterlevel": "预警级别",
"disastermsg": "预警类型"
}
}
}
}
}
@@ -0,0 +1,611 @@
import logging
from datetime import datetime, timedelta
import time
import asyncio
import async_timeout
import aiohttp
import voluptuous as vol
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.weather import (
WeatherEntity,
WeatherEntityFeature,
Forecast,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_FORECAST_HUMIDITY,
ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_NATIVE_WIND_SPEED,
PLATFORM_SCHEMA)
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_MODE,
#LENGTH_KILOMETERS,
UnitOfLength,
#PRESSURE_HPA,
UnitOfPressure,
#SPEED_KILOMETERS_PER_HOUR,
UnitOfSpeed,
#PRECIPITATION_MILLIMETERS_PER_HOUR,
UnitOfVolumetricFlux,
#TEMP_CELSIUS
UnitOfTemperature
)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from .heweather.const import (
DOMAIN,
CONF_AUTH_METHOD,
CONF_LONGITUDE,
CONF_LATITUDE,
CONF_HOST,
CONF_KEY,
CONF_JWT_SUB,
CONF_JWT_KID,
DEFAULT_HOST,
CONDITION_CLASSES,
ATTR_UPDATE_TIME,
ATTRIBUTION
)
_LOGGER = logging.getLogger(__name__)
TIME_BETWEEN_UPDATES = timedelta(seconds=1800)
HOURLY_TIME_BETWEEN_UPDATES = timedelta(seconds=1800)
DEFAULT_TIME = dt_util.now()
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_LONGITUDE): cv.string,
vol.Required(CONF_LATITUDE): cv.string,
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Required(CONF_KEY): cv.string,
})
# # 集成安装
# async def async_setup_entry(hass, config_entry, async_add_entities):
# _LOGGER.debug(f"register_static_path: {ROOT_PATH + ':custom_components/qweather/local'}")
# hass.http.register_static_path(ROOT_PATH, hass.config.path('custom_components/qweather/local'), False)
# hass.components.frontend.add_extra_js_url(hass, ROOT_PATH + '/qweather-card/qweather-card.js?ver=' + VERSION)
# hass.components.frontend.add_extra_js_url(hass, ROOT_PATH + '/qweather-card/qweather-more-info.js?ver=' + VERSION)
#
# _LOGGER.info("setup platform weather.Heweather...")
#
# name = config_entry.data.get(CONF_NAME)
# key = config_entry.data[CONF_API_KEY]
# location = config_entry.data[CONF_LOCATION]
# #unique_id = config_entry.unique_id
# longitude = round(config_entry.data[CONF_LONGITUDE],2)
# latitude = round(config_entry.data[CONF_LATITUDE],2)
# update_interval_minutes = config_entry.options.get(CONF_UPDATE_INTERVAL, 10)
# dailysteps = config_entry.options.get(CONF_DAILYSTEPS, 7)
# if dailysteps != 7 and dailysteps !=3:
# dailysteps = 7
# hourlysteps = config_entry.options.get(CONF_HOURLYSTEPS, 24)
# if hourlysteps != 24:
# hourlysteps = 24
# alert = config_entry.options.get(CONF_ALERT, True)
# life = config_entry.options.get(CONF_LIFEINDEX, True)
# starttime = config_entry.options.get(CONF_STARTTIME, 0)
# gird_weather = config_entry.options.get(CONF_GIRD, False)
#
# #data = WeatherData(hass, name, unique_id, api_key, longitude, latitude, dailysteps ,hourlysteps, alert, life, starttime, gird_weather)
# #location = config.get(CONF_LOCATION)
# #key = config.get(CONF_KEY)
# data = WeatherData(hass, location, key)
# await data.async_update(dt_util.now())
# async_track_time_interval(hass, data.async_update, timedelta(minutes = update_interval_minutes))
# _LOGGER.debug('[%s]刷新间隔时间: %s 分钟', name, update_interval_minutes)
# async_add_entities([HeWeather(data, location)], True)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
"""Set up the hefeng weather."""
_LOGGER.info("setup platform weather.Heweather...")
longitude = config_entry.data.get(CONF_LONGITUDE)
latitude = config_entry.data.get(CONF_LATITUDE)
host = config_entry.data.get(CONF_HOST)
auth_method = config_entry.data.get(CONF_AUTH_METHOD)
if auth_method == "key":
key = config_entry.data.get(CONF_KEY)
data = WeatherData(hass, longitude, latitude, host, key=key)
else:
# HeWeather Certification
heweather_cert = hass.data[DOMAIN].get('heweather_cert', None)
jwt_sub = config_entry.data.get(CONF_JWT_SUB)
jwt_kid = config_entry.data.get(CONF_JWT_KID)
data = WeatherData(hass, longitude, latitude, host, heweather_cert=heweather_cert, jwt_sub=jwt_sub, jwt_kid=jwt_kid)
weather = HeWeather(data, longitude, latitude)
await weather.async_update_data(dt_util.now())
config_entry.async_on_unload(async_track_time_interval(hass, weather.async_update_data, TIME_BETWEEN_UPDATES, cancel_on_shutdown=True))
async_add_entities([weather], True)
#@asyncio.coroutine
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the hefeng weather."""
_LOGGER.info("setup platform weather.Heweather...")
longitude = config.get(CONF_LONGITUDE)
latitude = config.get(CONF_LATITUDE)
host = config.get(CONF_HOST)
key = config.get(CONF_KEY)
data = WeatherData(hass, longitude, latitude, host, key=key)
weather = HeWeather(data, longitude, latitude)
await weather.async_update_data(dt_util.now())
async_track_time_interval(hass, weather.async_update_data, TIME_BETWEEN_UPDATES, cancel_on_shutdown=True)
async_add_devices([weather], True)
class HeWeather(WeatherEntity):
"""Representation of a weather condition."""
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_precipitation_unit = UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR
_attr_native_pressure_unit = UnitOfPressure.HPA
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
_attr_native_visibility_unit = UnitOfLength.KILOMETERS
def __init__(self, data, longitude, latitude):
"""Initialize the weather."""
# 【新增】开启实体命名支持
self._attr_has_entity_name = True
# 【新增】设置英文名,这样生成的 ID 就是 weather.home_heweather
self._attr_name = "heweather"
#self._name = None
self._object_id = 'localweather'
self._condition = None
self._temperature = None
self._humidity = None
self._pressure = None
self._wind_speed = None
self._wind_bearing = None
self._visibility = None
self._precipitation = None
self._forecast = None
self._forecast_hourly = None
self._dew = None
self._feelslike = None
self._cloud =None
self._data = data
self._updatetime = None
self._attr_unique_id = f"localweather_{longitude}_{latitude}"
self._attr_supported_features = 0
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
self._attr_supported_features |= WeatherEntityFeature.FORECAST_HOURLY
@property
def device_info(self):
"""Return the device info."""
return {
"identifiers": {(DOMAIN, f"{self._data._params['location']}")},
"name": "和风天气",
"manufacturer": "QWeather",
"model": "API v7",
}
# @property
# def name(self):
# """返回实体的名字."""
# return '和风天气'
@property
def should_poll(self):
"""attention No polling needed for a demo weather condition."""
return True
@property
def native_dew_point(self):
"""Return the native_dew_point."""
return self._dew
@property
def native_apparent_temperature(self):
"""Return the native_apparent_temperature."""
return self._feelslike
@property
def cloud_coverage(self):
"""Return the cloud_coverage."""
return self._cloud
@property
def native_temperature(self):
"""Return the temperature."""
return self._temperature
@property
def native_temperature_unit(self):
"""Return the unit of measurement."""
return self._attr_native_temperature_unit
@property
def humidity(self):
"""Return the humidity."""
return self._humidity
@property
def native_wind_speed(self):
"""Return the wind speed."""
return self._wind_speed
@property
def wind_bearing(self):
"""Return the wind speed."""
return self._wind_bearing
@property
def native_pressure(self):
"""Return the pressure."""
return self._pressure
@property
def native_visibility(self):
"""Return the visibility."""
return self._visibility
@property
def native_precipitation(self):
"""Return the precipitation."""
return self._precipitation
@property
def condition(self):
"""Return the weather condition."""
if self._condition:
match_list = [k for k, v in CONDITION_CLASSES.items() if self._condition in v]
return match_list[0] if match_list else 'unknown'
else:
return 'unknown'
# @property
# def attribution(self):
# """Return the attribution."""
# return 'Powered by Home Assistant'
# @property
# def device_state_attributes(self):
# """设置其它一些属性值."""
# if self._condition is not None:
# return {
# ATTR_ATTRIBUTION: ATTRIBUTION,
# ATTR_UPDATE_TIME: self._updatetime
# }
async def async_forecast_daily(self) -> list[Forecast]:
"""Return the daily forecast."""
# reftime = datetime.now()
reftime = self._data._updatetime
forecast_data = []
for entry in self._forecast:
data_dict = {
ATTR_FORECAST_TIME: reftime.isoformat(),
ATTR_FORECAST_CONDITION: entry[0],
ATTR_FORECAST_NATIVE_TEMP: entry[1],
ATTR_FORECAST_NATIVE_TEMP_LOW: entry[2],
'text': entry[3]
}
reftime = reftime + timedelta(days=1)
forecast_data.append(data_dict)
return forecast_data
async def async_forecast_hourly(self) -> list[Forecast]:
"""Return the daily forecast."""
# reftime = datetime.now()
reftime = self._data._updatetime
forecast_hourly_data = []
for entry in self._forecast_hourly:
data_dict = {
ATTR_FORECAST_TIME: reftime.isoformat(),
ATTR_FORECAST_CONDITION: entry[0],
ATTR_FORECAST_NATIVE_TEMP: entry[1],
ATTR_FORECAST_HUMIDITY: entry[2],
#"native_precipitation": entry[3],
ATTR_FORECAST_WIND_BEARING: entry[4],
ATTR_FORECAST_NATIVE_WIND_SPEED: entry[5],
"precipitation_probability": entry[6],
'text': entry[7]
}
#[forecast_hourly[0], float(hourlymsg[0]["temp"]), float(hourlymsg[0]["humidity"]), float(hourlymsg[0]["precip"]), hourlymsg[0]["windDir"], int(hourlymsg[0]["windSpeed"])],
reftime = reftime + timedelta(hours=1)
forecast_hourly_data.append(data_dict)
return forecast_hourly_data
#@asyncio.coroutine
async def async_update(self, now=DEFAULT_TIME):
"""update函数变成了async_update."""
self._updatetime = self._data.updatetime
#self._name = self._data.name
self._condition = self._data.condition
self._temperature = self._data.temperature
_attr_native_temperature_unit = self._data.temperature_unit
self._humidity = self._data.humidity
self._pressure = self._data.pressure
self._wind_speed = self._data.wind_speed
self._wind_bearing = self._data.wind_bearing
self._visibility = self._data.visibility
self._precipitation = self._data.precipitation
self._dew = self._data.dew
self._feelslike = self._data.feelslike
self._cloud = self._data.cloud
self._forecast = self._data.forecast
self._forecast_hourly = self._data.forecast_hourly
_LOGGER.info("success to update informations")
async def async_update_data(self, now=DEFAULT_TIME):
await self._data.async_update(now)
await self.async_update_listeners(['daily', 'hourly'])
class WeatherData():
"""天气相关的数据,存储在这个类中."""
def __init__(self, hass, longitude, latitude, host, key=None, heweather_cert=None, jwt_sub=None, jwt_kid=None):
"""初始化函数."""
self._hass = hass
location = f"{longitude},{latitude}"
if key is not None:
self._is_jwt = False
self._forecast_url = "https://"+host+"/v7/weather/7d?location="+location+"&key="+key
self._weather_now_url = "https://"+host+"/v7/weather/now?location="+location+"&key="+key
self._forecast_hourly_url = "https://"+host+"/v7/weather/24h?location="+location+"&key="+key
self._params = {"location": location,
"key": key}
else:
self._is_jwt = True
self._forecast_url = "https://"+host+"/v7/weather/7d?location="+location
self._weather_now_url = "https://"+host+"/v7/weather/now?location="+location
self._forecast_hourly_url = "https://"+host+"/v7/weather/24h?location="+location
self._params = {"location": location}
self._heweather_cert = heweather_cert
self._jwt_sub = jwt_sub
self._jwt_kid = jwt_kid
#self._name = None
self._condition = None
self._temperature = None
self._temperature_unit = None
self._humidity = None
self._pressure = None
self._wind_speed = None
self._wind_bearing = None
self._visibility = None
self._precipitation = None
self._dew = None
self._feelslike = None
self._cloud =None
self._forecast = None
self._forecast_hourly = None
self._updatetime = None
@property
def condition(self):
"""天气情况."""
return self._condition
@property
def temperature(self):
"""温度."""
return self._temperature
@property
def temperature_unit(self):
"""温度单位."""
return UnitOfTemperature.CELSIUS
@property
def humidity(self):
"""湿度."""
return self._humidity
@property
def pressure(self):
"""气压."""
return self._pressure
@property
def dew(self):
"""露点温度"""
return self._dew
@property
def feelslike(self):
"""体感温度"""
return self._feelslike
@property
def cloud(self):
"""云量"""
return self._cloud
@property
def wind_speed(self):
"""风速."""
return self._wind_speed
@property
def wind_bearing(self):
"""风向."""
return self._wind_bearing
@property
def visibility(self):
"""能见度."""
return self._visibility
@property
def precipitation (self):
"""当前小时累计降水量."""
return self._precipitation
@property
def forecast(self):
"""天预报."""
return self._forecast
@property
def forecast_hourly(self):
"""小时预报."""
return self._forecast_hourly
@property
def updatetime(self):
"""更新时间."""
return self._updatetime
#@asyncio.coroutine
async def async_update(self, now):
"""从远程更新信息."""
_LOGGER.info("Update from JingdongWangxiang's OpenAPI...")
"""
# 异步模式的测试代码
import time
_LOGGER.info("before time.sleep")
time.sleep(40)
_LOGGER.info("after time.sleep and before asyncio.sleep")
asyncio.sleep(40)
_LOGGER.info("after asyncio.sleep and before yield from asyncio.sleep")
#yield from
await asyncio.sleep(40)
_LOGGER.info("after yield from asyncio.sleep")
"""
# 通过HTTP访问,获取需要的信息
# 此处使用了基于aiohttp库的async_get_clientsession
try:
timeout = aiohttp.ClientTimeout(total=20)
connector = aiohttp.TCPConnector(limit=10)
headers = None
if self._is_jwt:
jwt_token = await self._heweather_cert.get_jwt_token_heweather_async(self._jwt_sub, self._jwt_kid, int(time.time()) - 30, int(time.time()) + 180)
headers = {'Authorization': f'Bearer {jwt_token}'}
async with aiohttp.ClientSession(connector=connector, timeout=timeout, headers=headers) as session:
async with session.get(self._weather_now_url) as response:
json_data = await response.json()
weather = json_data["now"]
async with session.get(self._forecast_url) as response:
json_data = await response.json()
forecast = json_data
async with session.get(self._forecast_hourly_url) as response:
json_data = await response.json()
forecast_hourly = json_data
except(asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error while accessing: %s", self._weather_now_url)
_LOGGER.error("Error while accessing: %s", self._forecast_url)
_LOGGER.error("Error while accessing: %s", self._forecast_hourly_url)
return
self._temperature = float(weather["temp"])
self._humidity = float(weather["humidity"])
self._pressure = weather["pressure"]
self._condition = weather["text"]
self._wind_speed = weather["windSpeed"]
self._wind_bearing = weather["windDir"]
self._visibility = weather["vis"]
self._precipitation = float(weather["precip"])
self._feelslike = float(weather["feelsLike"])
self._dew = float(weather["dew"])
self._cloud = int(weather["cloud"])
# self._windScale = weather["windScale"]
self._updatetime = datetime.strptime(weather["obsTime"], "%Y-%m-%dT%H:%M%z")
datemsg = forecast["daily"]
forec_cond = []
forec_text = []
for n in range(7):
for i, j in CONDITION_CLASSES.items():
if datemsg[n]["textDay"] in j:
forec_cond.append(i)
forec_text.append(datemsg[n]["textDay"])
self._forecast = [
[forec_cond[0], int(datemsg[0]["tempMax"]), int(datemsg[0]["tempMin"]), forec_text[0]],
[forec_cond[1], int(datemsg[1]["tempMax"]), int(datemsg[1]["tempMin"]), forec_text[1]],
[forec_cond[2], int(datemsg[2]["tempMax"]), int(datemsg[2]["tempMin"]), forec_text[2]],
[forec_cond[3], int(datemsg[3]["tempMax"]), int(datemsg[3]["tempMin"]), forec_text[3]],
[forec_cond[4], int(datemsg[4]["tempMax"]), int(datemsg[4]["tempMin"]), forec_text[4]],
[forec_cond[5], int(datemsg[5]["tempMax"]), int(datemsg[5]["tempMin"]), forec_text[5]],
[forec_cond[6], int(datemsg[6]["tempMax"]), int(datemsg[6]["tempMin"]), forec_text[6]]
]
hourlymsg = forecast_hourly["hourly"]
forecast_hourly = []
forec_text = []
for n in range(24):
for i, j in CONDITION_CLASSES.items():
if hourlymsg[n]["text"] in j:
forecast_hourly.append(i)
forec_text.append(hourlymsg[n]["text"])
self._forecast_hourly = [
[forecast_hourly[0], float(hourlymsg[0]["temp"]), float(hourlymsg[0]["humidity"]), float(hourlymsg[0]["precip"]), hourlymsg[0]["windDir"], int(hourlymsg[0]["windSpeed"]), float(hourlymsg[0]["pop"]), forec_text[0]],
[forecast_hourly[1], float(hourlymsg[1]["temp"]), float(hourlymsg[1]["humidity"]), float(hourlymsg[1]["precip"]), hourlymsg[1]["windDir"], int(hourlymsg[1]["windSpeed"]), float(hourlymsg[1]["pop"]), forec_text[1]],
[forecast_hourly[2], float(hourlymsg[2]["temp"]), float(hourlymsg[2]["humidity"]), float(hourlymsg[2]["precip"]), hourlymsg[2]["windDir"], int(hourlymsg[2]["windSpeed"]), float(hourlymsg[2]["pop"]), forec_text[2]],
[forecast_hourly[3], float(hourlymsg[3]["temp"]), float(hourlymsg[3]["humidity"]), float(hourlymsg[3]["precip"]), hourlymsg[3]["windDir"], int(hourlymsg[3]["windSpeed"]), float(hourlymsg[3]["pop"]), forec_text[3]],
[forecast_hourly[4], float(hourlymsg[4]["temp"]), float(hourlymsg[4]["humidity"]), float(hourlymsg[4]["precip"]), hourlymsg[4]["windDir"], int(hourlymsg[4]["windSpeed"]), float(hourlymsg[4]["pop"]), forec_text[4]],
[forecast_hourly[5], float(hourlymsg[5]["temp"]), float(hourlymsg[5]["humidity"]), float(hourlymsg[5]["precip"]), hourlymsg[5]["windDir"], int(hourlymsg[5]["windSpeed"]), float(hourlymsg[5]["pop"]), forec_text[5]],
[forecast_hourly[6], float(hourlymsg[6]["temp"]), float(hourlymsg[6]["humidity"]), float(hourlymsg[6]["precip"]), hourlymsg[6]["windDir"], int(hourlymsg[6]["windSpeed"]), float(hourlymsg[6]["pop"]), forec_text[6]],
[forecast_hourly[7], float(hourlymsg[7]["temp"]), float(hourlymsg[7]["humidity"]), float(hourlymsg[7]["precip"]), hourlymsg[7]["windDir"], int(hourlymsg[7]["windSpeed"]), float(hourlymsg[7]["pop"]), forec_text[7]],
[forecast_hourly[8], float(hourlymsg[8]["temp"]), float(hourlymsg[8]["humidity"]), float(hourlymsg[8]["precip"]), hourlymsg[8]["windDir"], int(hourlymsg[8]["windSpeed"]), float(hourlymsg[8]["pop"]), forec_text[8]],
[forecast_hourly[9], float(hourlymsg[9]["temp"]), float(hourlymsg[9]["humidity"]), float(hourlymsg[9]["precip"]), hourlymsg[9]["windDir"], int(hourlymsg[9]["windSpeed"]), float(hourlymsg[9]["pop"]), forec_text[9]],
[forecast_hourly[10], float(hourlymsg[10]["temp"]), float(hourlymsg[10]["humidity"]), float(hourlymsg[10]["precip"]), hourlymsg[10]["windDir"], int(hourlymsg[10]["windSpeed"]), float(hourlymsg[10]["pop"]), forec_text[10]],
[forecast_hourly[11], float(hourlymsg[11]["temp"]), float(hourlymsg[11]["humidity"]), float(hourlymsg[11]["precip"]), hourlymsg[11]["windDir"], int(hourlymsg[11]["windSpeed"]), float(hourlymsg[11]["pop"]), forec_text[11]],
[forecast_hourly[12], float(hourlymsg[12]["temp"]), float(hourlymsg[12]["humidity"]), float(hourlymsg[12]["precip"]), hourlymsg[12]["windDir"], int(hourlymsg[12]["windSpeed"]), float(hourlymsg[12]["pop"]), forec_text[12]],
[forecast_hourly[13], float(hourlymsg[13]["temp"]), float(hourlymsg[13]["humidity"]), float(hourlymsg[13]["precip"]), hourlymsg[13]["windDir"], int(hourlymsg[13]["windSpeed"]), float(hourlymsg[13]["pop"]), forec_text[13]],
[forecast_hourly[14], float(hourlymsg[14]["temp"]), float(hourlymsg[14]["humidity"]), float(hourlymsg[14]["precip"]), hourlymsg[14]["windDir"], int(hourlymsg[14]["windSpeed"]), float(hourlymsg[14]["pop"]), forec_text[14]],
[forecast_hourly[15], float(hourlymsg[15]["temp"]), float(hourlymsg[15]["humidity"]), float(hourlymsg[15]["precip"]), hourlymsg[15]["windDir"], int(hourlymsg[15]["windSpeed"]), float(hourlymsg[15]["pop"]), forec_text[15]],
[forecast_hourly[16], float(hourlymsg[16]["temp"]), float(hourlymsg[16]["humidity"]), float(hourlymsg[16]["precip"]), hourlymsg[16]["windDir"], int(hourlymsg[16]["windSpeed"]), float(hourlymsg[16]["pop"]), forec_text[16]],
[forecast_hourly[17], float(hourlymsg[17]["temp"]), float(hourlymsg[17]["humidity"]), float(hourlymsg[17]["precip"]), hourlymsg[17]["windDir"], int(hourlymsg[17]["windSpeed"]), float(hourlymsg[17]["pop"]), forec_text[17]],
[forecast_hourly[18], float(hourlymsg[18]["temp"]), float(hourlymsg[18]["humidity"]), float(hourlymsg[18]["precip"]), hourlymsg[18]["windDir"], int(hourlymsg[18]["windSpeed"]), float(hourlymsg[18]["pop"]), forec_text[18]],
[forecast_hourly[19], float(hourlymsg[19]["temp"]), float(hourlymsg[19]["humidity"]), float(hourlymsg[19]["precip"]), hourlymsg[19]["windDir"], int(hourlymsg[19]["windSpeed"]), float(hourlymsg[19]["pop"]), forec_text[19]],
[forecast_hourly[20], float(hourlymsg[20]["temp"]), float(hourlymsg[20]["humidity"]), float(hourlymsg[20]["precip"]), hourlymsg[20]["windDir"], int(hourlymsg[20]["windSpeed"]), float(hourlymsg[20]["pop"]), forec_text[20]],
[forecast_hourly[21], float(hourlymsg[21]["temp"]), float(hourlymsg[21]["humidity"]), float(hourlymsg[21]["precip"]), hourlymsg[21]["windDir"], int(hourlymsg[21]["windSpeed"]), float(hourlymsg[21]["pop"]), forec_text[21]],
[forecast_hourly[22], float(hourlymsg[22]["temp"]), float(hourlymsg[22]["humidity"]), float(hourlymsg[22]["precip"]), hourlymsg[22]["windDir"], int(hourlymsg[22]["windSpeed"]), float(hourlymsg[22]["pop"]), forec_text[22]],
[forecast_hourly[23], float(hourlymsg[23]["temp"]), float(hourlymsg[23]["humidity"]), float(hourlymsg[23]["precip"]), hourlymsg[23]["windDir"], int(hourlymsg[23]["windSpeed"]), float(hourlymsg[23]["pop"]), forec_text[23]]
]
_LOGGER.info("success to load local informations")