初始化config文件

This commit is contained in:
2026-08-24 21:55:33 +08:00
commit 80a05d172e
2983 changed files with 1221334 additions and 0 deletions
@@ -0,0 +1,92 @@
"""The Colorfulclouds integration."""
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
CONF_ALERT,
CONF_DAILYSTEPS,
CONF_HOURLYSTEPS,
CONF_STARTTIME,
CONF_INTERVAL,
COORDINATOR,
DOMAIN,
UNDO_UPDATE_LISTENER,
)
from .colorfulclouds import ColorfulcloudsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.WEATHER, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Colorfulclouds as config entry."""
hass.data.setdefault(DOMAIN, {})
api_key = config_entry.options.get(
CONF_API_KEY
) or config_entry.data.get(CONF_API_KEY, "")
location_key = config_entry.unique_id
longitude = config_entry.data[CONF_LONGITUDE]
latitude = config_entry.data[CONF_LATITUDE]
api_version = config_entry.options.get(
CONF_API_VERSION
) or config_entry.data.get(CONF_API_VERSION, "v2.5")
dailysteps = config_entry.options.get(CONF_DAILYSTEPS, 5)
hourlysteps = config_entry.options.get(CONF_HOURLYSTEPS, 24)
alert = config_entry.options.get(CONF_ALERT, True)
starttime = config_entry.options.get(CONF_STARTTIME, 0)
interval = config_entry.options.get(CONF_INTERVAL, 10)
websession = async_get_clientsession(hass)
coordinator = ColorfulcloudsDataUpdateCoordinator(
hass,
websession,
api_key,
api_version,
location_key,
longitude,
latitude,
dailysteps,
hourlysteps,
alert,
starttime,
interval,
)
await coordinator.async_config_entry_first_refresh()
undo_listener = config_entry.add_update_listener(update_listener)
hass.data[DOMAIN][config_entry.entry_id] = {
COORDINATOR: coordinator,
UNDO_UPDATE_LISTENER: undo_listener,
}
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
if unload_ok:
entry_data = hass.data[DOMAIN].pop(config_entry.entry_id)
entry_data[UNDO_UPDATE_LISTENER]()
return unload_ok
async def update_listener(hass, config_entry):
"""Update listener."""
await hass.config_entries.async_reload(config_entry.entry_id)
@@ -0,0 +1,97 @@
"""Data update coordinator for the Colorfulclouds integration."""
import asyncio
import datetime
import logging
from aiohttp import ClientError
from async_timeout import timeout
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class ColorfulcloudsDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Colorfulclouds data API."""
def __init__(
self,
hass,
websession,
api_key,
api_version,
location_key,
longitude,
latitude,
dailysteps: int,
hourlysteps: int,
alert: bool,
starttime: int,
interval: int,
):
"""Initialize."""
self.location_key = location_key
self.longitude = longitude
self.latitude = latitude
self.dailysteps = dailysteps
self.alert = alert
self.interval = interval
self.hourlysteps = hourlysteps
self.api_version = api_version
self.api_key = api_key
self.starttime = starttime
self.websession = websession
self.is_metric = "metric:v2" if hass.config.units is METRIC_SYSTEM else "imperial"
update_interval = datetime.timedelta(minutes=self.interval)
_LOGGER.debug("Data will be updated every %s", update_interval)
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
def _build_url(self, start_timestamp: int) -> str:
"""Build the Colorfulclouds API URL."""
return (
f"https://api.caiyunapp.com/{self.api_version}/{self.api_key}/"
f"{self.longitude},{self.latitude}/weather.json"
f"?dailysteps={self.dailysteps}&hourlysteps={self.hourlysteps}"
f"&alert={str(self.alert).lower()}&unit={self.is_metric}"
f"&timestamp={start_timestamp}"
)
async def _async_update_data(self):
"""Update data via the Colorfulclouds API."""
start_timestamp = int(
(
datetime.datetime.now() + datetime.timedelta(days=self.starttime)
).timestamp()
)
url = self._build_url(start_timestamp)
try:
async with timeout(10):
resp = await self.websession.get(url)
resdata = await resp.json()
except (ClientError, asyncio.TimeoutError) as error:
raise UpdateFailed(
f"Error communicating with Colorfulclouds API: {error}"
) from error
except ValueError as error:
raise UpdateFailed(
f"Invalid response from Colorfulclouds API: {error}"
) from error
if resdata.get("status") != "ok":
raise UpdateFailed(
"Colorfulclouds API returned an error: "
f"{resdata.get('error') or resdata.get('status')}"
)
_LOGGER.debug("Colorfulclouds data refreshed successfully")
return {
**resdata,
"location_key": self.location_key,
"is_metric": self.is_metric,
}
@@ -0,0 +1,200 @@
"""Config flow for the Colorfulclouds integration."""
from __future__ import annotations
from collections import OrderedDict
import json
import logging
import requests
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from .const import (
CONF_ALERT,
CONF_DAILYSTEPS,
CONF_HOURLYSTEPS,
CONF_STARTTIME,
CONF_INTERVAL,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
def _fetch_api_data(url):
"""Fetch and parse JSON from the given URL."""
json_text = requests.get(url, timeout=10).content
return json.loads(json_text)
class ColorfulcloudsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Colorfulclouds."""
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return ColorfulcloudsOptionsFlow()
def __init__(self):
"""Initialize."""
self._errors = {}
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
self._errors = {}
if user_input is None:
return await self._show_config_form(user_input)
# Check if the location is already configured.
existing = await self._check_existing(user_input[CONF_NAME])
if existing:
return self.async_abort(reason="already_configured")
# Test the API credentials.
url = (
f"https://api.caiyunapp.com/{user_input[CONF_API_VERSION]}/"
f"{user_input[CONF_API_KEY]}/{user_input['longitude']},"
f"{user_input['latitude']}/daily.json"
)
try:
redata = await self.hass.async_add_executor_job(_fetch_api_data, url)
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
_LOGGER.warning("Unable to connect to Colorfulclouds API: %s", error)
self._errors["base"] = "cannot_connect"
return await self._show_config_form(user_input)
if redata.get("status") != "ok":
self._errors["base"] = "communication"
return await self._show_config_form(user_input)
await self.async_set_unique_id(
f"{user_input['longitude']}-{user_input['latitude']}".replace(".", "_")
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
async def _show_config_form(self, user_input):
"""Show the configuration form to the user."""
# Defaults
api_version = "v2.5"
data_schema = OrderedDict()
data_schema[vol.Required(CONF_API_KEY)] = str
data_schema[vol.Optional(CONF_API_VERSION, default=api_version)] = str
data_schema[
vol.Optional(CONF_LONGITUDE, default=self.hass.config.longitude)
] = cv.longitude
data_schema[vol.Optional(CONF_LATITUDE, default=self.hass.config.latitude)] = (
cv.latitude
)
data_schema[vol.Optional(CONF_NAME, default=self.hass.config.location_name)] = (
str
)
return self.async_show_form(
step_id="user", data_schema=vol.Schema(data_schema), errors=self._errors
)
async def _check_existing(self, host):
for entry in self._async_current_entries():
if host == entry.data.get(CONF_NAME):
return True
return False
class ColorfulcloudsOptionsFlow(config_entries.OptionsFlow):
"""Config flow options for Colorfulclouds."""
def __init__(self):
"""Initialize."""
self._errors = {}
@property
def _api_key(self):
"""Return the configured API key, options take precedence."""
return self.config_entry.options.get(
CONF_API_KEY
) or self.config_entry.data.get(CONF_API_KEY, "")
@property
def _api_version(self):
"""Return the configured API version, options take precedence."""
return self.config_entry.options.get(
CONF_API_VERSION
) or self.config_entry.data.get(CONF_API_VERSION, "v2.5")
async def async_step_init(self, user_input=None):
"""Manage the options."""
return await self.async_step_user(user_input)
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
self._errors = {}
if user_input is not None:
# Test the API credentials before saving.
api_key = user_input.get(CONF_API_KEY) or self._api_key
api_version = user_input.get(CONF_API_VERSION) or self._api_version
url = (
f"https://api.caiyunapp.com/{api_version}/{api_key}/"
f"{self.config_entry.data[CONF_LONGITUDE]},"
f"{self.config_entry.data[CONF_LATITUDE]}/daily.json"
)
try:
redata = await self.hass.async_add_executor_job(_fetch_api_data, url)
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
_LOGGER.warning("Unable to connect to Colorfulclouds API: %s", error)
self._errors["base"] = "cannot_connect"
return await self._show_options_form(user_input)
if redata.get("status") != "ok":
self._errors["base"] = "communication"
return await self._show_options_form(user_input)
return self.async_create_entry(title="", data=user_input)
return await self._show_options_form(user_input)
async def _show_options_form(self, user_input):
"""Show the options form."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Optional(CONF_API_KEY, default=self._api_key): str,
vol.Optional(
CONF_API_VERSION, default=self._api_version
): str,
vol.Optional(
CONF_DAILYSTEPS,
default=self.config_entry.options.get(CONF_DAILYSTEPS, 5),
): vol.All(vol.Coerce(int), vol.Range(min=5, max=15)),
vol.Optional(
CONF_INTERVAL,
default=self.config_entry.options.get(CONF_INTERVAL, 10),
): vol.All(vol.Coerce(int), vol.Range(min=5)),
vol.Optional(
CONF_HOURLYSTEPS,
default=self.config_entry.options.get(CONF_HOURLYSTEPS, 24),
): vol.All(vol.Coerce(int), vol.Range(min=24, max=360)),
vol.Optional(
CONF_STARTTIME,
default=self.config_entry.options.get(CONF_STARTTIME, 0),
): vol.All(vol.Coerce(int), vol.Range(min=-5, max=0)),
vol.Optional(
CONF_ALERT,
default=self.config_entry.options.get(CONF_ALERT, True),
): bool,
}
),
errors=self._errors,
)
@@ -0,0 +1,105 @@
"""Constants for the colorfulClouds integration."""
DOMAIN = "colorfulclouds"
CONF_ALERT = "alert"
CONF_HOURLYSTEPS = "hourlysteps"
CONF_DAILYSTEPS = "dailysteps"
CONF_STARTTIME = "starttime"
CONF_INTERVAL = "interval"
COORDINATOR = "coordinator"
UNDO_UPDATE_LISTENER = "undo_update_listener"
ATTRIBUTION = "Data provided by Colorfulclouds"
ATTR_ICON = "icon"
ATTR_FORECAST = "forecast"
ATTR_LABEL = "label"
ATTR_UNIT_IMPERIAL = "Imperial"
ATTR_UNIT_METRIC = "Metric"
MANUFACTURER = "Colorfulclouds, Inc."
NAME = "Colorfulclouds"
from homeassistant.const import ATTR_DEVICE_CLASS, DEGREE, UV_INDEX
OPTIONAL_SENSORS = ()
SENSOR_TYPES = {
"apparent_temperature": {
ATTR_DEVICE_CLASS: "temperature",
ATTR_ICON: None,
ATTR_LABEL: "感觉温度",
ATTR_UNIT_METRIC: "°C",
ATTR_UNIT_IMPERIAL: "°F",
},
"temperature": {
ATTR_DEVICE_CLASS: "temperature",
ATTR_ICON: None,
ATTR_LABEL: "温度",
ATTR_UNIT_METRIC: "°C",
ATTR_UNIT_IMPERIAL: "°F",
},
"cloudrate": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-cloudy",
ATTR_LABEL: "云量",
ATTR_UNIT_METRIC: "%",
ATTR_UNIT_IMPERIAL: "%",
},
"precipitation": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-rainy",
ATTR_LABEL: "雨量",
ATTR_UNIT_METRIC: "mm",
ATTR_UNIT_IMPERIAL: "in",
},
"pressure": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:gauge",
ATTR_LABEL: "气压",
ATTR_UNIT_METRIC: "Pa",
ATTR_UNIT_IMPERIAL: "Pa",
},
"comfort": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:gauge",
ATTR_LABEL: "舒适指数",
ATTR_UNIT_METRIC: None,
ATTR_UNIT_IMPERIAL: None,
},
"ultraviolet": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-sunny",
ATTR_LABEL: "紫外线",
ATTR_UNIT_METRIC: UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX,
},
"humidity": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:water-percent",
ATTR_LABEL: "湿度",
ATTR_UNIT_METRIC: "%",
ATTR_UNIT_IMPERIAL: "%",
},
"visibility": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-fog",
ATTR_LABEL: "能见度",
ATTR_UNIT_METRIC: "km",
ATTR_UNIT_IMPERIAL: "mi",
},
"WindSpeed": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-windy",
ATTR_LABEL: "风速",
ATTR_UNIT_METRIC: "km/h",
ATTR_UNIT_IMPERIAL: "mph",
},
"WindDirection": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:windsock",
ATTR_LABEL: "风向",
ATTR_UNIT_METRIC: DEGREE,
ATTR_UNIT_IMPERIAL: DEGREE,
},
}
@@ -0,0 +1,15 @@
{
"domain": "colorfulclouds",
"name": "colorfulClouds",
"codeowners": ["@fineembs"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/fineemb/Colorfulclouds-weather",
"issue_tracker": "https://github.com/fineemb/Colorfulclouds-weather/issues",
"homekit": {},
"iot_class": "cloud_polling",
"requirements": [],
"version": "2.1.1",
"ssdp": [],
"zeroconf": []
}
@@ -0,0 +1,178 @@
"""Support for the Colorfulclouds service."""
import logging
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_NAME
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import Entity
from .const import (
ATTR_ICON,
ATTR_LABEL,
ATTRIBUTION,
COORDINATOR,
DOMAIN,
MANUFACTURER,
OPTIONAL_SENSORS,
SENSOR_TYPES,
)
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add Colorfulclouds entities from a config_entry."""
name = config_entry.data[CONF_NAME]
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
sensors = []
for sensor in SENSOR_TYPES:
sensors.append(ColorfulcloudsSensor(name, sensor, coordinator))
async_add_entities(sensors, False)
class ColorfulcloudsSensor(Entity):
"""Define an Colorfulclouds entity."""
def __init__(self, name, kind, coordinator, forecast_day=None):
"""Initialize."""
self._name = name
self.kind = kind
self.coordinator = coordinator
self._device_class = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._unit_system = (
"Metric"
if self.coordinator.data["is_metric"] == "metric:v2"
else "Imperial"
)
self.forecast_day = forecast_day
@property
def name(self):
"""Return the name."""
if self.forecast_day is not None:
return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]} {self.forecast_day}d"
return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
@property
def unique_id(self):
"""Return a unique_id for this entity."""
# _LOGGER.info("sensor_unique_id: %s", self.coordinator.data["location_key"])
return f"{self.coordinator.data['location_key']}-{self.kind}".lower()
@property
def device_info(self):
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self.coordinator.data["location_key"])},
"name": self._name,
"manufacturer": MANUFACTURER,
"entry_type": DeviceEntryType.SERVICE,
}
@property
def should_poll(self):
"""Return the polling requirement of the entity."""
return False
@property
def available(self):
"""Return True if entity is available."""
return self.coordinator.last_update_success
@property
def state(self):
"""Return the state."""
try:
realtime = self.coordinator.data["result"]["realtime"]
if self.kind == "apparent_temperature":
return realtime[self.kind]
if self.kind == "pressure":
return realtime[self.kind]
if self.kind == "temperature":
return realtime[self.kind]
if self.kind == "humidity":
return round(float(realtime[self.kind]) * 100)
if self.kind == "cloudrate":
return round(float(realtime[self.kind]) * 100)
if self.kind == "visibility":
return realtime[self.kind]
if self.kind == "WindSpeed":
return realtime["wind"]["speed"]
if self.kind == "WindDirection":
return realtime["wind"]["direction"]
if self.kind == "comfort":
return realtime["life_index"]["comfort"]["index"]
if self.kind == "ultraviolet":
return realtime["life_index"]["ultraviolet"]["index"]
if self.kind == "precipitation":
return realtime["precipitation"]["local"]["intensity"]
except (KeyError, TypeError):
return None
return None
@property
def icon(self):
"""Return the icon."""
# if self.forecast_day is not None:
# return FORECAST_SENSOR_TYPES[self.kind][ATTR_ICON]
return SENSOR_TYPES[self.kind][ATTR_ICON]
@property
def device_class(self):
"""Return the device_class."""
# if self.forecast_day is not None:
# return FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
# if self.forecast_day is not None:
# return FORECAST_SENSOR_TYPES[self.kind][self._unit_system]
return SENSOR_TYPES[self.kind][self._unit_system]
@property
def extra_state_attributes(self):
"""Return the state attributes."""
realtime = self.coordinator.data["result"]["realtime"]
if self.kind == "ultraviolet":
self._attrs["desc"] = (
realtime.get("life_index", {})
.get("ultraviolet", {})
.get("desc", "")
)
elif self.kind == "comfort":
self._attrs["desc"] = (
realtime.get("life_index", {}).get("comfort", {}).get("desc", "")
)
elif self.kind == "precipitation":
precipitation = realtime.get("precipitation", {})
self._attrs["datasource"] = (
precipitation.get("local", {}).get("datasource", "")
)
self._attrs["nearest_intensity"] = (
precipitation.get("nearest", {}).get("intensity", "")
)
self._attrs["nearest_distance"] = (
precipitation.get("nearest", {}).get("distance", "")
)
return self._attrs
@property
def entity_registry_enabled_default(self):
"""Return if the entity should be enabled when first added to the entity registry."""
return bool(self.kind not in OPTIONAL_SENSORS)
async def async_added_to_hass(self):
"""Connect to dispatcher listening for entity data notifications."""
self.async_on_remove(
self.coordinator.async_add_listener(self.async_write_ha_state)
)
async def async_update(self):
"""Update Colorfulclouds entity."""
await self.coordinator.async_request_refresh()
@@ -0,0 +1,45 @@
{
"config": {
"step": {
"user": {
"title": "Colorful Clouds",
"description": "If you need help with configuration, check here: https://github.com/fineemb/Colorfulclouds-weather",
"data": {
"api_key": "API Key",
"api_version": "API version, the default is v2.5",
"latitude": "Latitude",
"longitude": "Longitude",
"name": "Name"
}
}
},
"error": {
"cannot_connect": "Cannot connect to the Colorfulclouds API.",
"communication": "The key may be invalid, please check the input value."
},
"abort": {
"already_configured": "This location is already configured."
}
},
"options": {
"step": {
"user": {
"title": "Colorful Clouds options",
"description": "Set the number of days you need to get forecast data, as well as extreme weather warning data",
"data": {
"api_key": "API Key",
"api_version": "API version, the default is v2.5",
"dailysteps": "Day forecast (5-15)",
"hourlysteps": "Hourly forecast (24-360)",
"alert": "Extreme weather warnings",
"starttime": "The forecast start time, -1 is yesterday, -2 is the day before yesterday",
"interval": "Refresh interval in minutes"
}
}
},
"error": {
"cannot_connect": "Cannot connect to the Colorfulclouds API.",
"communication": "The key may be invalid, please check the input value."
}
}
}
@@ -0,0 +1,47 @@
{
"config": {
"title": "Colorful Clouds",
"step": {
"user": {
"title": "Colorful Clouds",
"description": "If you need help with configuration, check here: https://github.com/fineemb/Colorfulclouds-weather",
"data": {
"api_key": "API Key",
"api_version": "API version,The default is v2.5",
"latitude" : "Latitude",
"longitude" : "Longitude",
"name" : "Name"
}
}
},
"error": {
"cannot_connect": "Cannot connect to the Colorfulclouds API.",
"communication": "The key may be invalid, please check the input value."
},
"abort": {
"single_instance_allowed": "Only a single configuration is allowed.",
"already_configured": "Do not repeat the configuration."
}
},
"options": {
"step": {
"user": {
"title": "Colorful Clouds options",
"data": {
"api_key": "API Key",
"api_version": "API version, the default is v2.5",
"dailysteps": "Day Forecast (5-15)",
"hourlysteps": "Hourly forecast (24-360)",
"alert": "Extreme weather warnings",
"starttime": "The forecast start time, -1 is yesterday, -2 is the day before yesterday",
"interval": "Refresh interval in minutes"
},
"description": "Set the number of days you need to get forecast data, as well as extreme weather warning data"
}
},
"error": {
"cannot_connect": "Cannot connect to the Colorfulclouds API.",
"communication": "The key may be invalid, please check the input value."
}
}
}
@@ -0,0 +1,47 @@
{
"config": {
"title": "彩云天气",
"step": {
"user": {
"title": "彩云天气",
"description": "如果您需要配置方面的帮助,请查看此处: https://github.com/fineemb/Colorfulclouds-weather",
"data": {
"api_key": "彩云API Key",
"api_version": "API版本,默认是v2.5",
"latitude" : "维度",
"longitude" : "经度",
"name" : "名称"
}
}
},
"error": {
"cannot_connect": "无法连接到彩云天气 API。",
"communication": "Key可能无效,请检查输入值."
},
"abort": {
"single_instance_allowed": "仅允许单个配置.",
"already_configured": "请勿重复配置."
}
},
"options": {
"step": {
"user": {
"title": "彩云天气选项",
"data": {
"api_key": "彩云API Key",
"api_version": "API版本,默认v2.5",
"dailysteps": "天级预报(5-15)",
"hourlysteps": "小时级预报(24-360)",
"alert": "极端天气预警",
"starttime": "预报起始时间,-1 是昨天,-2 是前天",
"interval": "定时刷新数据间隔(分钟)"
},
"description": "设置你需要获取预报数据的天数,以及极端天气预警数据"
}
},
"error": {
"cannot_connect": "无法连接到彩云天气 API。",
"communication": "Key可能无效,请检查输入值."
}
}
}
@@ -0,0 +1,327 @@
from __future__ import annotations
import logging
from homeassistant.components.weather import (
ATTR_CONDITION_HAIL,
ATTR_CONDITION_CLOUDY,
ATTR_CONDITION_FOG,
ATTR_CONDITION_LIGHTNING_RAINY,
ATTR_CONDITION_PARTLYCLOUDY,
ATTR_CONDITION_RAINY,
ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SNOWY_RAINY,
ATTR_CONDITION_SUNNY,
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_WINDY,
ATTR_CONDITION_POURING,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_HUMIDITY,
ATTR_WEATHER_CLOUD_COVERAGE,
ATTR_WEATHER_HUMIDITY,
ATTR_WEATHER_PRESSURE,
ATTR_WEATHER_VISIBILITY,
Forecast,
WeatherEntity,
WeatherEntityFeature,
)
from homeassistant.const import CONF_NAME
from homeassistant.helpers.device_registry import DeviceEntryType
from .const import ATTRIBUTION, COORDINATOR, DOMAIN, MANUFACTURER
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
CONDITION_MAP = {
"CLEAR_DAY": ATTR_CONDITION_SUNNY,
"CLEAR_NIGHT": ATTR_CONDITION_CLEAR_NIGHT,
"PARTLY_CLOUDY_DAY": ATTR_CONDITION_PARTLYCLOUDY,
"PARTLY_CLOUDY_NIGHT": ATTR_CONDITION_PARTLYCLOUDY,
"CLOUDY": ATTR_CONDITION_CLOUDY,
"LIGHT_HAZE": ATTR_CONDITION_FOG,
"MODERATE_HAZE": ATTR_CONDITION_FOG,
"HEAVY_HAZE": ATTR_CONDITION_FOG,
"LIGHT_RAIN": ATTR_CONDITION_RAINY,
"MODERATE_RAIN": ATTR_CONDITION_RAINY,
"HEAVY_RAIN": ATTR_CONDITION_POURING,
"STORM_RAIN": ATTR_CONDITION_POURING,
"FOG": ATTR_CONDITION_FOG,
"LIGHT_SNOW": ATTR_CONDITION_SNOWY,
"MODERATE_SNOW": ATTR_CONDITION_SNOWY,
"HEAVY_SNOW": ATTR_CONDITION_SNOWY,
"STORM_SNOW": ATTR_CONDITION_SNOWY,
"DUST": ATTR_CONDITION_FOG,
"SAND": ATTR_CONDITION_FOG,
"THUNDER_SHOWER": ATTR_CONDITION_LIGHTNING_RAINY,
"HAIL": ATTR_CONDITION_HAIL,
"SLEET": ATTR_CONDITION_SNOWY_RAINY,
"WIND": ATTR_CONDITION_WINDY,
"HAZE": ATTR_CONDITION_FOG,
"RAIN": ATTR_CONDITION_RAINY,
"SNOW": ATTR_CONDITION_SNOWY,
"FREEZE": ATTR_CONDITION_SNOWY_RAINY,
"BLOWING_SAND": ATTR_CONDITION_WINDY,
}
def _get_condition(skycon: str, default: str | None = None) -> str | None:
"""Map a Colorfulclouds skycon to a Home Assistant weather condition."""
return CONDITION_MAP.get(skycon, default)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add a Colorfulclouds weather entity from a config_entry."""
name = config_entry.data[CONF_NAME]
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
# _LOGGER.debug("metric: %s", coordinator.data["is_metric"])
async_add_entities([ColorfulCloudsEntity(name, coordinator)], True)
class ColorfulCloudsEntity(WeatherEntity):
"""Representation of a weather condition."""
_attr_supported_features = (
WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_DAILY
)
def __init__(self, name, coordinator):
self.coordinator = coordinator
_LOGGER.debug("coordinator: %s", coordinator.data["server_time"])
self._name = name
self._attrs = {}
@property
def name(self):
return self._name
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
@property
def unique_id(self):
"""Return a unique_id for this entity."""
_LOGGER.debug("weather_unique_id: %s", self.coordinator.data["location_key"])
return self.coordinator.data["location_key"]
@property
def device_info(self):
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self.coordinator.data["location_key"])},
"name": self._name,
"manufacturer": MANUFACTURER,
"entry_type": DeviceEntryType.SERVICE,
}
@property
def should_poll(self):
"""Return the polling requirement of the entity."""
return False
@property
def available(self):
"""Return True if entity is available."""
return self.coordinator.last_update_success
@property
def state(self):
"""Return the weather condition."""
skycon = self.coordinator.data["result"]["realtime"]["skycon"]
return _get_condition(skycon)
@property
def cloud_coverage(self):
return round(
float(self.coordinator.data["result"]["realtime"]["cloudrate"]) * 100
)
@property
def condition(self):
"""Return the weather condition."""
skycon = self.coordinator.data["result"]["realtime"]["skycon"]
return _get_condition(skycon)
@property
def humidity(self):
return round(float(self.coordinator.data["result"]["realtime"]["humidity"]) * 100)
@property
def native_apparent_temperature(self):
return self.coordinator.data["result"]["realtime"]["apparent_temperature"]
@property
def native_precipitation_unit(self):
return "mm" if self.coordinator.data["is_metric"] == "metric:v2" else "in"
@property
def native_pressure(self):
return self.coordinator.data["result"]["realtime"]["pressure"]
@property
def native_pressure_unit(self):
return "Pa"
@property
def native_temperature(self):
return self.coordinator.data["result"]["realtime"]["temperature"]
@property
def native_temperature_unit(self):
return "°C" if self.coordinator.data["is_metric"] == "metric:v2" else "°F"
@property
def native_visibility(self):
"""能见度"""
return self.coordinator.data["result"]["realtime"]["visibility"]
@property
def native_visibility_unit(self):
return "km" if self.coordinator.data["is_metric"] == "metric:v2" else "mi"
@property
def native_wind_speed(self):
"""风速"""
return self.coordinator.data["result"]["realtime"]["wind"]["speed"]
@property
def wind_bearing(self):
"""风向"""
return self.coordinator.data["result"]["realtime"]["wind"]["direction"]
@property
def native_wind_speed_unit(self):
return "km/h" if self.coordinator.data["is_metric"] == "metric:v2" else "mph"
@property
def uv_index(self):
"""紫外线"""
return self.coordinator.data["result"]["realtime"]["life_index"]["ultraviolet"][
"index"
]
@property
def extra_state_attributes(self):
result = self.coordinator.data["result"]
realtime = result["realtime"]
minutely = result.get("minutely", {})
air_quality = realtime.get("air_quality", {})
aqi = air_quality.get("aqi", {})
aqi_description = air_quality.get("description", {})
self._attrs = {
"skycon": realtime["skycon"],
"forecast_hourly": result.get("hourly", {}).get("description", ""),
"forecast_minutely": minutely.get("description", ""),
"forecast_probability": minutely.get("probability", ""),
"forecast_keypoint": result.get("forecast_keypoint", ""),
"forecast_alert": result.get("alert", ""),
"pm25": air_quality.get("pm25", ""),
"pm10": air_quality.get("pm10", ""),
"o3": air_quality.get("o3", ""),
"no2": air_quality.get("no2", ""),
"so2": air_quality.get("so2", ""),
"co": air_quality.get("co", ""),
"aqi": aqi.get("chn", ""),
"aqi_description": aqi_description.get("chn", ""),
"aqi_usa": aqi.get("usa", ""),
"aqi_usa_description": aqi_description.get("usa", ""),
}
return self._attrs
async def async_added_to_hass(self):
"""Connect to dispatcher listening for entity data notifications."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.async_add_listener(self.async_write_ha_state)
)
async def async_update(self):
"""Update Colorfulclouds entity."""
await self.coordinator.async_request_refresh()
async def async_forecast_daily(self) -> list[Forecast] | None:
daily = self.coordinator.data["result"]["daily"]
forecast_data = []
try:
for i in range(len(daily["temperature"])):
skycon = daily["skycon"][i]["value"]
forecast_data.append(
{
ATTR_FORECAST_TIME: daily["temperature"][i]["date"],
"skycon": skycon,
ATTR_FORECAST_CONDITION: _get_condition(
skycon, ATTR_CONDITION_CLOUDY
),
ATTR_FORECAST_NATIVE_TEMP: daily["temperature"][i]["max"],
ATTR_FORECAST_NATIVE_TEMP_LOW: daily["temperature"][i][
"min"
],
ATTR_FORECAST_WIND_BEARING: daily["wind"][i]["avg"][
"direction"
],
ATTR_FORECAST_NATIVE_WIND_SPEED: daily["wind"][i]["avg"][
"speed"
],
ATTR_FORECAST_PRECIPITATION: daily["precipitation"][i][
"avg"
],
ATTR_FORECAST_HUMIDITY: round(
float(daily["humidity"][i]["avg"]) * 100
),
ATTR_WEATHER_CLOUD_COVERAGE: round(
float(daily["cloudrate"][i]["avg"]) * 100
),
ATTR_WEATHER_PRESSURE: daily["pressure"][i]["avg"],
ATTR_WEATHER_VISIBILITY: daily["visibility"][i]["avg"],
}
)
except (KeyError, IndexError, TypeError) as error:
_LOGGER.warning("Unable to build daily forecast: %s", error)
return forecast_data
async def async_forecast_hourly(self) -> list[Forecast] | None:
hourly = self.coordinator.data["result"]["hourly"]
forecast_data = []
try:
for i in range(len(hourly["temperature"])):
skycon = hourly["skycon"][i]["value"]
forecast_data.append(
{
ATTR_FORECAST_TIME: hourly["temperature"][i]["datetime"],
"skycon": skycon,
ATTR_FORECAST_CONDITION: _get_condition(
skycon, ATTR_CONDITION_CLOUDY
),
ATTR_FORECAST_NATIVE_TEMP: hourly["temperature"][i][
"value"
],
ATTR_FORECAST_WIND_BEARING: hourly["wind"][i]["direction"],
ATTR_FORECAST_NATIVE_WIND_SPEED: hourly["wind"][i]["speed"],
ATTR_FORECAST_PRECIPITATION: hourly["precipitation"][i][
"value"
],
ATTR_WEATHER_HUMIDITY: round(
float(hourly["humidity"][i]["value"]) * 100
),
ATTR_WEATHER_CLOUD_COVERAGE: round(
float(hourly["cloudrate"][i]["value"]) * 100
),
ATTR_WEATHER_PRESSURE: hourly["pressure"][i]["value"],
ATTR_WEATHER_VISIBILITY: hourly["visibility"][i]["value"],
}
)
except (KeyError, IndexError, TypeError) as error:
_LOGGER.warning("Unable to build hourly forecast: %s", error)
return forecast_data