初始化config文件
This commit is contained in:
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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={})
|
||||
Binary file not shown.
BIN
Binary file not shown.
@@ -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")
|
||||
Reference in New Issue
Block a user