19 files
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -105,9 +105,9 @@ class PirateWeatherConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
latitude = user_input[CONF_LATITUDE]
|
latitude = user_input[CONF_LATITUDE]
|
||||||
longitude = user_input[CONF_LONGITUDE]
|
longitude = user_input[CONF_LONGITUDE]
|
||||||
forecastMode = "daily"
|
forecast_mode = "daily"
|
||||||
forecastPlatform = user_input[PW_PLATFORM]
|
forecast_platform = user_input[PW_PLATFORM]
|
||||||
entityNamee = user_input[CONF_NAME]
|
entity_name = user_input[CONF_NAME]
|
||||||
endpoint = user_input[CONF_ENDPOINT]
|
endpoint = user_input[CONF_ENDPOINT]
|
||||||
|
|
||||||
# Convert scan interval to timedelta
|
# Convert scan interval to timedelta
|
||||||
@@ -125,7 +125,7 @@ class PirateWeatherConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
# Unique value includes the location and forcastHours/ forecastDays to seperate WeatherEntity/ Sensor
|
# Unique value includes the location and forcastHours/ forecastDays to seperate WeatherEntity/ Sensor
|
||||||
# await self.async_set_unique_id(f"pw-{latitude}-{longitude}-{forecastDays}-{forecastHours}-{forecastMode}-{entityNamee}")
|
# await self.async_set_unique_id(f"pw-{latitude}-{longitude}-{forecastDays}-{forecastHours}-{forecastMode}-{entityNamee}")
|
||||||
await self.async_set_unique_id(
|
await self.async_set_unique_id(
|
||||||
f"pw-{latitude}-{longitude}-{forecastPlatform}-{forecastMode}-{entityNamee}"
|
f"pw-{latitude}-{longitude}-{forecast_platform}-{forecast_mode}-{entity_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
@@ -328,8 +328,10 @@ class PirateWeatherOptionsFlow(OptionsFlow):
|
|||||||
|
|
||||||
|
|
||||||
async def _is_pw_api_online(hass, api_key, lat, lon, endpoint):
|
async def _is_pw_api_online(hass, api_key, lat, lon, endpoint):
|
||||||
forecastString = endpoint + "/forecast/" + api_key + "/" + str(lat) + "," + str(lon)
|
forecast_string = (
|
||||||
|
endpoint + "/forecast/" + api_key + "/" + str(lat) + "," + str(lon)
|
||||||
|
)
|
||||||
|
|
||||||
session = async_get_clientsession(hass)
|
session = async_get_clientsession(hass)
|
||||||
async with session.get(forecastString) as resp:
|
async with session.get(forecast_string) as resp:
|
||||||
return resp.status
|
return resp.status
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ class Forecast(UnicodeMixin):
|
|||||||
self.json = data
|
self.json = data
|
||||||
|
|
||||||
self._alerts = []
|
self._alerts = []
|
||||||
for alertJSON in self.json.get("alerts", []):
|
for alert_json in self.json.get("alerts", []):
|
||||||
self._alerts.append(Alert(alertJSON))
|
self._alerts.append(Alert(alert_json))
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update the forecast data by making a new request to the same URL."""
|
"""Update the forecast data by making a new request to the same URL."""
|
||||||
@@ -135,24 +135,24 @@ class PirateWeatherFlagsBlock(UnicodeMixin):
|
|||||||
class PirateWeatherDataPoint(UnicodeMixin):
|
class PirateWeatherDataPoint(UnicodeMixin):
|
||||||
"""Represent a single data point in a weather forecast, such as an hourly or daily data point."""
|
"""Represent a single data point in a weather forecast, such as an hourly or daily data point."""
|
||||||
|
|
||||||
def __init__(self, d={}):
|
def __init__(self, d=None):
|
||||||
"""Initialize the data point with timestamp and weather information."""
|
"""Initialize the data point with timestamp and weather information."""
|
||||||
self.d = d
|
self.d = d or {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.time = datetime.datetime.fromtimestamp(int(d["time"]))
|
self.time = datetime.datetime.fromtimestamp(int(self.d["time"]))
|
||||||
self.utime = d["time"]
|
self.utime = self.d["time"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sr_time = int(d["sunriseTime"])
|
sr_time = int(self.d["sunriseTime"])
|
||||||
self.sunriseTime = datetime.datetime.fromtimestamp(sr_time)
|
self.sunriseTime = datetime.datetime.fromtimestamp(sr_time)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.sunriseTime = None
|
self.sunriseTime = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ss_time = int(d["sunsetTime"])
|
ss_time = int(self.d["sunsetTime"])
|
||||||
self.sunsetTime = datetime.datetime.fromtimestamp(ss_time)
|
self.sunsetTime = datetime.datetime.fromtimestamp(ss_time)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.sunsetTime = None
|
self.sunsetTime = None
|
||||||
|
|||||||
@@ -10,5 +10,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"issue_tracker": "https://github.com/alexander0042/pirate-weather-ha/issues",
|
"issue_tracker": "https://github.com/alexander0042/pirate-weather-ha/issues",
|
||||||
"version": "1.9.0"
|
"version": "1.9.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from homeassistant.components.sensor import (
|
|||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION,
|
ATTR_ATTRIBUTION,
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
CONF_LONGITUDE,
|
CONF_LONGITUDE,
|
||||||
@@ -27,6 +26,7 @@ from homeassistant.const import (
|
|||||||
DEGREE,
|
DEGREE,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
UV_INDEX,
|
UV_INDEX,
|
||||||
|
UnitOfDensity,
|
||||||
UnitOfIrradiance,
|
UnitOfIrradiance,
|
||||||
UnitOfLength,
|
UnitOfLength,
|
||||||
UnitOfPressure,
|
UnitOfPressure,
|
||||||
@@ -451,11 +451,11 @@ SENSOR_TYPES: dict[str, PirateWeatherSensorEntityDescription] = {
|
|||||||
name="Smoke",
|
name="Smoke",
|
||||||
device_class=SensorDeviceClass.PM25,
|
device_class=SensorDeviceClass.PM25,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
si_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
si_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
us_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
us_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
ca_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
ca_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
uk_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
uk_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
uk2_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
uk2_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
icon="mdi:smoke",
|
icon="mdi:smoke",
|
||||||
forecast_mode=["currently", "hourly"],
|
forecast_mode=["currently", "hourly"],
|
||||||
@@ -465,11 +465,11 @@ SENSOR_TYPES: dict[str, PirateWeatherSensorEntityDescription] = {
|
|||||||
name="Smoke Max",
|
name="Smoke Max",
|
||||||
device_class=SensorDeviceClass.PM25,
|
device_class=SensorDeviceClass.PM25,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
si_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
si_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
us_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
us_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
ca_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
ca_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
uk_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
uk_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
uk2_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
uk2_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
icon="mdi:smoke",
|
icon="mdi:smoke",
|
||||||
forecast_mode=["daily"],
|
forecast_mode=["daily"],
|
||||||
@@ -1034,22 +1034,22 @@ async def async_setup_entry(
|
|||||||
service_id = config_entry.unique_id or config_entry.entry_id
|
service_id = config_entry.unique_id or config_entry.entry_id
|
||||||
|
|
||||||
# Round Output
|
# Round Output
|
||||||
outputRound = domain_data[PW_ROUND]
|
output_round = domain_data[PW_ROUND]
|
||||||
|
|
||||||
sensors: list[PirateWeatherSensor] = []
|
sensors: list[PirateWeatherSensor] = []
|
||||||
|
|
||||||
for condition in conditions:
|
for condition in conditions:
|
||||||
# Save units for conversion later
|
# Save units for conversion later
|
||||||
requestUnits = domain_data[CONF_UNITS]
|
request_units = domain_data[CONF_UNITS]
|
||||||
|
|
||||||
sensorDescription = SENSOR_TYPES[condition]
|
sensor_description = SENSOR_TYPES[condition]
|
||||||
|
|
||||||
if condition in DEPRECATED_SENSOR_TYPES:
|
if condition in DEPRECATED_SENSOR_TYPES:
|
||||||
_LOGGER.warning("Monitored condition %s is deprecated", condition)
|
_LOGGER.warning("Monitored condition %s is deprecated", condition)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not sensorDescription.forecast_mode
|
not sensor_description.forecast_mode
|
||||||
or "currently" in sensorDescription.forecast_mode
|
or "currently" in sensor_description.forecast_mode
|
||||||
):
|
):
|
||||||
unique_id = f"{config_entry.unique_id}-sensor-{condition}"
|
unique_id = f"{config_entry.unique_id}-sensor-{condition}"
|
||||||
sensors.append(
|
sensors.append(
|
||||||
@@ -1060,14 +1060,14 @@ async def async_setup_entry(
|
|||||||
unique_id,
|
unique_id,
|
||||||
forecast_day=None,
|
forecast_day=None,
|
||||||
forecast_hour=None,
|
forecast_hour=None,
|
||||||
description=sensorDescription,
|
description=sensor_description,
|
||||||
requestUnits=requestUnits,
|
request_units=request_units,
|
||||||
outputRound=outputRound,
|
output_round=output_round,
|
||||||
service_id=service_id,
|
service_id=service_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if forecast_days is not None and "daily" in sensorDescription.forecast_mode:
|
if forecast_days is not None and "daily" in sensor_description.forecast_mode:
|
||||||
for forecast_day in forecast_days:
|
for forecast_day in forecast_days:
|
||||||
unique_id = (
|
unique_id = (
|
||||||
f"{config_entry.unique_id}-sensor-{condition}-daily-{forecast_day}"
|
f"{config_entry.unique_id}-sensor-{condition}-daily-{forecast_day}"
|
||||||
@@ -1080,14 +1080,14 @@ async def async_setup_entry(
|
|||||||
unique_id,
|
unique_id,
|
||||||
forecast_day=int(forecast_day),
|
forecast_day=int(forecast_day),
|
||||||
forecast_hour=None,
|
forecast_hour=None,
|
||||||
description=sensorDescription,
|
description=sensor_description,
|
||||||
requestUnits=requestUnits,
|
request_units=request_units,
|
||||||
outputRound=outputRound,
|
output_round=output_round,
|
||||||
service_id=service_id,
|
service_id=service_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if forecast_hours is not None and "hourly" in sensorDescription.forecast_mode:
|
if forecast_hours is not None and "hourly" in sensor_description.forecast_mode:
|
||||||
for forecast_h in forecast_hours:
|
for forecast_h in forecast_hours:
|
||||||
unique_id = (
|
unique_id = (
|
||||||
f"{config_entry.unique_id}-sensor-{condition}-hourly-{forecast_h}"
|
f"{config_entry.unique_id}-sensor-{condition}-hourly-{forecast_h}"
|
||||||
@@ -1100,9 +1100,9 @@ async def async_setup_entry(
|
|||||||
unique_id,
|
unique_id,
|
||||||
forecast_day=None,
|
forecast_day=None,
|
||||||
forecast_hour=int(forecast_h),
|
forecast_hour=int(forecast_h),
|
||||||
description=sensorDescription,
|
description=sensor_description,
|
||||||
requestUnits=requestUnits,
|
request_units=request_units,
|
||||||
outputRound=outputRound,
|
output_round=output_round,
|
||||||
service_id=service_id,
|
service_id=service_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1117,7 +1117,7 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
_attr_attribution = ATTRIBUTION
|
_attr_attribution = ATTRIBUTION
|
||||||
entity_description: PirateWeatherSensorEntityDescription
|
entity_description: PirateWeatherSensorEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__( # noqa: PLR0917
|
||||||
self,
|
self,
|
||||||
weather_coordinator: WeatherUpdateCoordinator,
|
weather_coordinator: WeatherUpdateCoordinator,
|
||||||
condition: str,
|
condition: str,
|
||||||
@@ -1126,8 +1126,8 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
forecast_day: int,
|
forecast_day: int,
|
||||||
forecast_hour: int,
|
forecast_hour: int,
|
||||||
description: PirateWeatherSensorEntityDescription,
|
description: PirateWeatherSensorEntityDescription,
|
||||||
requestUnits: str,
|
request_units: str,
|
||||||
outputRound: str,
|
output_round: str,
|
||||||
service_id: str,
|
service_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
@@ -1149,8 +1149,8 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
|
|
||||||
self.forecast_day = forecast_day
|
self.forecast_day = forecast_day
|
||||||
self.forecast_hour = forecast_hour
|
self.forecast_hour = forecast_hour
|
||||||
self.requestUnits = requestUnits
|
self.request_units = request_units
|
||||||
self.outputRound = outputRound
|
self.output_round = output_round
|
||||||
self.type = condition
|
self.type = condition
|
||||||
self._icon = None
|
self._icon = None
|
||||||
self._alerts = None
|
self._alerts = None
|
||||||
@@ -1188,7 +1188,7 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
@property
|
@property
|
||||||
def unit_system(self):
|
def unit_system(self):
|
||||||
"""Return the unit system of this entity."""
|
"""Return the unit system of this entity."""
|
||||||
return self.requestUnits
|
return self.request_units
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entity_picture(self) -> str | None:
|
def entity_picture(self) -> str | None:
|
||||||
@@ -1223,11 +1223,11 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
def extra_state_attributes(self):
|
def extra_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
if self.type == "alerts":
|
if self.type == "alerts":
|
||||||
extraATTR = self._alerts
|
extra_attr = self._alerts
|
||||||
extraATTR[ATTR_ATTRIBUTION] = ATTRIBUTION
|
extra_attr[ATTR_ATTRIBUTION] = ATTRIBUTION
|
||||||
else:
|
else:
|
||||||
extraATTR = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
extra_attr = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||||
return extraATTR
|
return extra_attr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
@@ -1249,15 +1249,15 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
dkey = f"{attr}_{i!s}"
|
dkey = f"{attr}_{i!s}"
|
||||||
else:
|
else:
|
||||||
dkey = attr
|
dkey = attr
|
||||||
alertsAttr = getattr(alert, attr)
|
alerts_attr = getattr(alert, attr)
|
||||||
|
|
||||||
# Convert time to string using dt_util
|
# Convert time to string using dt_util
|
||||||
if isinstance(alertsAttr, int):
|
if isinstance(alerts_attr, int):
|
||||||
alertsAttr = dt_util.as_local(
|
alerts_attr = dt_util.as_local(
|
||||||
dt_util.utc_from_timestamp(alertsAttr)
|
dt_util.utc_from_timestamp(alerts_attr)
|
||||||
).isoformat()
|
).isoformat()
|
||||||
|
|
||||||
alerts[dkey] = alertsAttr
|
alerts[dkey] = alerts_attr
|
||||||
|
|
||||||
self._alerts = alerts
|
self._alerts = alerts
|
||||||
native_val = len(data)
|
native_val = len(data)
|
||||||
@@ -1342,12 +1342,12 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
self._icon = getattr(data, "icon", "")
|
self._icon = getattr(data, "icon", "")
|
||||||
|
|
||||||
# If output rounding is requested, round to nearest integer
|
# If output rounding is requested, round to nearest integer
|
||||||
if self.outputRound == "Yes":
|
if self.output_round == "Yes":
|
||||||
roundingVal = 0
|
rounding_val = 0
|
||||||
roundingPrecip = 2
|
rounding_precip = 2
|
||||||
else:
|
else:
|
||||||
roundingVal = 2
|
rounding_val = 2
|
||||||
roundingPrecip = 4
|
rounding_precip = 4
|
||||||
|
|
||||||
# Some state data needs to be rounded to whole values or converted to
|
# Some state data needs to be rounded to whole values or converted to
|
||||||
# percentages
|
# percentages
|
||||||
@@ -1364,10 +1364,10 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
"sunset_time",
|
"sunset_time",
|
||||||
"time",
|
"time",
|
||||||
]:
|
]:
|
||||||
outState = datetime.datetime.fromtimestamp(state, datetime.UTC)
|
out_state = datetime.datetime.fromtimestamp(state, datetime.UTC)
|
||||||
|
|
||||||
elif self.type == "fire_risk_level":
|
elif self.type == "fire_risk_level":
|
||||||
outState = fire_index(state)
|
out_state = fire_index(state)
|
||||||
elif self.type in [
|
elif self.type in [
|
||||||
"dew_point",
|
"dew_point",
|
||||||
"temperature",
|
"temperature",
|
||||||
@@ -1395,10 +1395,10 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
"solar",
|
"solar",
|
||||||
"solar_max",
|
"solar_max",
|
||||||
]:
|
]:
|
||||||
if roundingVal == 0:
|
if rounding_val == 0:
|
||||||
outState = int(round(state, roundingVal))
|
out_state = int(round(state, rounding_val))
|
||||||
else:
|
else:
|
||||||
outState = round(state, roundingVal)
|
out_state = round(state, rounding_val)
|
||||||
|
|
||||||
elif self.type in [
|
elif self.type in [
|
||||||
"precip_accumulation",
|
"precip_accumulation",
|
||||||
@@ -1423,12 +1423,12 @@ class PirateWeatherSensor(SensorEntity):
|
|||||||
and self.unit_system != "us"
|
and self.unit_system != "us"
|
||||||
):
|
):
|
||||||
state = state * 10
|
state = state * 10
|
||||||
outState = round(state, roundingPrecip)
|
out_state = round(state, rounding_precip)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
outState = state
|
out_state = state
|
||||||
|
|
||||||
return outState
|
return out_state
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Connect to dispatcher listening for entity data notifications."""
|
"""Connect to dispatcher listening for entity data notifications."""
|
||||||
@@ -1455,16 +1455,16 @@ def fire_index(fire_index):
|
|||||||
"""Convert numeric fire index to a textual value."""
|
"""Convert numeric fire index to a textual value."""
|
||||||
|
|
||||||
if fire_index == -999:
|
if fire_index == -999:
|
||||||
outState = "N/A"
|
out_state = "N/A"
|
||||||
elif fire_index >= 30:
|
elif fire_index >= 30:
|
||||||
outState = "Extreme"
|
out_state = "Extreme"
|
||||||
elif fire_index >= 20:
|
elif fire_index >= 20:
|
||||||
outState = "Very High"
|
out_state = "Very High"
|
||||||
elif fire_index >= 10:
|
elif fire_index >= 10:
|
||||||
outState = "High"
|
out_state = "High"
|
||||||
elif fire_index >= 5:
|
elif fire_index >= 5:
|
||||||
outState = "Moderate"
|
out_state = "Moderate"
|
||||||
else:
|
else:
|
||||||
outState = "Low"
|
out_state = "Low"
|
||||||
|
|
||||||
return outState
|
return out_state
|
||||||
|
|||||||
@@ -267,10 +267,10 @@ async def async_setup_entry(
|
|||||||
service_id = config_entry.unique_id or config_entry.entry_id
|
service_id = config_entry.unique_id or config_entry.entry_id
|
||||||
|
|
||||||
# Round Output
|
# Round Output
|
||||||
outputRound = domain_data[PW_ROUND]
|
output_round = domain_data[PW_ROUND]
|
||||||
|
|
||||||
pw_weather = PirateWeather(
|
pw_weather = PirateWeather(
|
||||||
name, unique_id, forecast_mode, weather_coordinator, outputRound, service_id
|
name, unique_id, forecast_mode, weather_coordinator, output_round, service_id
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities([pw_weather], False)
|
async_add_entities([pw_weather], False)
|
||||||
@@ -288,13 +288,13 @@ class PirateWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
|
|||||||
| WeatherEntityFeature.FORECAST_HOURLY
|
| WeatherEntityFeature.FORECAST_HOURLY
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__( # noqa: PLR0917
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
unique_id,
|
unique_id,
|
||||||
forecast_mode: str,
|
forecast_mode: str,
|
||||||
weather_coordinator: WeatherUpdateCoordinator,
|
weather_coordinator: WeatherUpdateCoordinator,
|
||||||
outputRound: str,
|
output_round: str,
|
||||||
service_id: str,
|
service_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
@@ -315,7 +315,7 @@ class PirateWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
|
|||||||
self._ds_hourly = self._weather_coordinator.data.hourly()
|
self._ds_hourly = self._weather_coordinator.data.hourly()
|
||||||
self._ds_daily = self._weather_coordinator.data.daily()
|
self._ds_daily = self._weather_coordinator.data.daily()
|
||||||
|
|
||||||
self.outputRound = outputRound
|
self.output_round = output_round
|
||||||
|
|
||||||
units = WEATHER_UNITS.get(
|
units = WEATHER_UNITS.get(
|
||||||
self._weather_coordinator.requested_units, WEATHER_UNITS["si"]
|
self._weather_coordinator.requested_units, WEATHER_UNITS["si"]
|
||||||
@@ -369,11 +369,13 @@ class PirateWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
|
|||||||
@property
|
@property
|
||||||
def cloud_coverage(self):
|
def cloud_coverage(self):
|
||||||
"""Return the cloud coverage."""
|
"""Return the cloud coverage."""
|
||||||
cloudCover = (
|
cloud_cover = self._weather_coordinator.data.currently().d.get("cloudCover")
|
||||||
self._weather_coordinator.data.currently().d.get("cloudCover") * 100.0
|
|
||||||
)
|
|
||||||
|
|
||||||
return round(cloudCover, 2) if cloudCover != -999 else None
|
return (
|
||||||
|
round(cloud_cover * 100, 2)
|
||||||
|
if cloud_cover is not None and cloud_cover != -999
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def humidity(self):
|
def humidity(self):
|
||||||
@@ -399,16 +401,16 @@ class PirateWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
|
|||||||
@property
|
@property
|
||||||
def native_wind_gust_speed(self):
|
def native_wind_gust_speed(self):
|
||||||
"""Return the wind gust speed."""
|
"""Return the wind gust speed."""
|
||||||
windGust = self._weather_coordinator.data.currently().d.get("windGust")
|
wind_gust = self._weather_coordinator.data.currently().d.get("windGust")
|
||||||
|
|
||||||
return round(windGust, 2) if windGust != -999 else None
|
return round(wind_gust, 2) if wind_gust != -999 else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wind_bearing(self):
|
def wind_bearing(self):
|
||||||
"""Return the wind bearing."""
|
"""Return the wind bearing."""
|
||||||
windBearing = self._weather_coordinator.data.currently().d.get("windBearing")
|
wind_bearing = self._weather_coordinator.data.currently().d.get("windBearing")
|
||||||
|
|
||||||
return windBearing if windBearing != -999 else None
|
return wind_bearing if wind_bearing != -999 else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ozone(self):
|
def ozone(self):
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ ATTRIBUTION = "Powered by Pirate Weather"
|
|||||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Weather data update coordinator."""
|
"""Weather data update coordinator."""
|
||||||
|
|
||||||
def __init__(
|
def __init__( # noqa: PLR0917
|
||||||
self,
|
self,
|
||||||
api_key,
|
api_key,
|
||||||
latitude,
|
latitude,
|
||||||
@@ -72,25 +72,27 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
"""Poll weather data from PW."""
|
"""Poll weather data from PW."""
|
||||||
|
|
||||||
if self.latitude == 0.0:
|
if self.latitude == 0.0:
|
||||||
requestLatitude = self.hass.config.latitude
|
request_latitude = self.hass.config.latitude
|
||||||
else:
|
else:
|
||||||
requestLatitude = self.latitude
|
request_latitude = self.latitude
|
||||||
|
|
||||||
if self.longitude == 0.0:
|
if self.longitude == 0.0:
|
||||||
requestLongitude = self.hass.config.longitude
|
request_longitude = self.hass.config.longitude
|
||||||
else:
|
else:
|
||||||
requestLongitude = self.longitude
|
request_longitude = self.longitude
|
||||||
|
|
||||||
_LOGGER.debug("Request coordinates: %s, %s", requestLatitude, requestLongitude)
|
_LOGGER.debug(
|
||||||
|
"Request coordinates: %s, %s", request_latitude, request_longitude
|
||||||
|
)
|
||||||
|
|
||||||
forecastString = (
|
forecast_string = (
|
||||||
self.endpoint
|
self.endpoint
|
||||||
+ "/forecast/"
|
+ "/forecast/"
|
||||||
+ self._api_key
|
+ self._api_key
|
||||||
+ "/"
|
+ "/"
|
||||||
+ str(requestLatitude)
|
+ str(request_latitude)
|
||||||
+ ","
|
+ ","
|
||||||
+ str(requestLongitude)
|
+ str(request_longitude)
|
||||||
+ "?units="
|
+ "?units="
|
||||||
+ self.requested_units
|
+ self.requested_units
|
||||||
+ "&extend=hourly"
|
+ "&extend=hourly"
|
||||||
@@ -104,12 +106,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
m.strip() for m in self.models.split(",") if m.strip()
|
m.strip() for m in self.models.split(",") if m.strip()
|
||||||
)
|
)
|
||||||
if exclusions:
|
if exclusions:
|
||||||
forecastString += "&exclude=" + exclusions
|
forecast_string += "&exclude=" + exclusions
|
||||||
|
|
||||||
session = async_get_clientsession(self.hass)
|
session = async_get_clientsession(self.hass)
|
||||||
async with session.get(forecastString) as resp:
|
async with session.get(forecast_string) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
jsonText = await resp.json()
|
json_text = await resp.json()
|
||||||
headers = resp.headers
|
headers = resp.headers
|
||||||
_LOGGER.debug("Pirate Weather data update from: %s", self.endpoint)
|
_LOGGER.debug("Pirate Weather data update from: %s", self.endpoint)
|
||||||
return Forecast(jsonText, resp, headers)
|
return Forecast(json_text, resp, headers)
|
||||||
|
|||||||
@@ -205,6 +205,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
# Instantiate all features manager
|
# Instantiate all features manager
|
||||||
self._managers: list[BaseFeatureManager] = []
|
self._managers: list[BaseFeatureManager] = []
|
||||||
|
|
||||||
|
# Names of feature managers provided by external plugins that have
|
||||||
|
# already been instantiated for this thermostat. Used to avoid
|
||||||
|
# registering the same external manager twice (post_init + startup retry).
|
||||||
|
self._external_manager_names: set[str] = set()
|
||||||
|
|
||||||
|
# Instances of feature managers provided by external plugins. They are
|
||||||
|
# also present in ``self._managers`` (for lifecycle) but are tracked
|
||||||
|
# separately so they can be refreshed on every control cycle (internal
|
||||||
|
# managers have their own dedicated per-cycle mechanisms).
|
||||||
|
self._external_managers: list[BaseFeatureManager] = []
|
||||||
|
|
||||||
self._presence_manager: FeaturePresenceManager = FeaturePresenceManager(
|
self._presence_manager: FeaturePresenceManager = FeaturePresenceManager(
|
||||||
self, hass
|
self, hass
|
||||||
)
|
)
|
||||||
@@ -237,6 +248,42 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
"""Register a manager"""
|
"""Register a manager"""
|
||||||
self._managers.append(manager)
|
self._managers.append(manager)
|
||||||
|
|
||||||
|
def _load_external_feature_managers(self):
|
||||||
|
"""Instantiate feature managers provided by external plugins.
|
||||||
|
|
||||||
|
Query the VTherm API registry and create one manager instance per
|
||||||
|
eligible thermostat (the factory decides eligibility through its
|
||||||
|
``supports`` method). Managers whose plugin registers after this
|
||||||
|
thermostat has been built are picked up later during ``async_startup``,
|
||||||
|
mirroring the external proportional algorithm retry behavior.
|
||||||
|
"""
|
||||||
|
api = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||||
|
if api is None or not hasattr(api, "get_feature_manager_factories"):
|
||||||
|
return
|
||||||
|
|
||||||
|
for factory in api.get_feature_manager_factories():
|
||||||
|
name = factory.name
|
||||||
|
if name in self._external_manager_names:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if not factory.supports(self):
|
||||||
|
continue
|
||||||
|
manager = factory.create(self)
|
||||||
|
manager.post_init(self._entry_infos)
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
_LOGGER.error(
|
||||||
|
"%s - Error while creating external feature manager '%s': %s",
|
||||||
|
self,
|
||||||
|
name,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.register_manager(manager)
|
||||||
|
self._external_manager_names.add(name)
|
||||||
|
self._external_managers.append(manager)
|
||||||
|
_LOGGER.info("%s - Registered external feature manager '%s'", self, name)
|
||||||
|
|
||||||
def clean_central_config_doublon(
|
def clean_central_config_doublon(
|
||||||
self, config_entry: ConfigData, central_config: ConfigEntry | None
|
self, config_entry: ConfigData, central_config: ConfigEntry | None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -308,6 +355,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
for manager in self._managers:
|
for manager in self._managers:
|
||||||
manager.post_init(entry_infos)
|
manager.post_init(entry_infos)
|
||||||
|
|
||||||
|
# Instantiate feature managers provided by external plugins that are
|
||||||
|
# already registered at this point. Late-registered plugins are handled
|
||||||
|
# by a retry in async_startup.
|
||||||
|
self._load_external_feature_managers()
|
||||||
|
|
||||||
self._use_central_config_temperature = entry_infos.get(
|
self._use_central_config_temperature = entry_infos.get(
|
||||||
CONF_USE_PRESETS_CENTRAL_CONFIG
|
CONF_USE_PRESETS_CENTRAL_CONFIG
|
||||||
) or (
|
) or (
|
||||||
@@ -487,6 +539,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||||
# need_write_state = False
|
# need_write_state = False
|
||||||
|
|
||||||
|
# Retry loading external feature managers: a plugin may have registered
|
||||||
|
# its factory after this thermostat was built (load order between the
|
||||||
|
# core and the plugin is not guaranteed). Newly created managers are
|
||||||
|
# then started in the loop below.
|
||||||
|
self._load_external_feature_managers()
|
||||||
|
|
||||||
# start listening for all managers
|
# start listening for all managers
|
||||||
for manager in self._managers:
|
for manager in self._managers:
|
||||||
await manager.start_listening()
|
await manager.start_listening()
|
||||||
@@ -898,6 +956,30 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
return self._state_manager.current_state.target_temperature
|
return self._state_manager.current_state.target_temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def regulated_target_temperature(self) -> float | None:
|
||||||
|
"""Return the regulated target temperature used to drive the underlying.
|
||||||
|
|
||||||
|
The base implementation returns the plain target temperature. Over
|
||||||
|
climate thermostats override this to expose their regulated value.
|
||||||
|
"""
|
||||||
|
return self.target_temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def underlying_fan_modes(self) -> list[str] | None:
|
||||||
|
"""Return the fan modes exposed by the underlying climate(s).
|
||||||
|
|
||||||
|
The base implementation returns None because most thermostats do not
|
||||||
|
expose an underlying climate with fan control.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_set_underlying_fan_mode(self, fan_mode: str) -> None:
|
||||||
|
"""Send a fan mode to the underlying climate(s).
|
||||||
|
|
||||||
|
The base implementation is a no-op for thermostats without fan control.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> ClimateEntityFeature:
|
def supported_features(self) -> ClimateEntityFeature:
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
@@ -1609,6 +1691,25 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
# Call specific control heating
|
# Call specific control heating
|
||||||
await self._control_heating_specific(timestamp, force)
|
await self._control_heating_specific(timestamp, force)
|
||||||
|
|
||||||
|
# Refresh external feature managers (provided by plugins) on every cycle.
|
||||||
|
# This is done after the specific control heating so that a manager can
|
||||||
|
# react to the regulated target temperature (e.g. drive the underlying
|
||||||
|
# fan mode), and before the publication block below so that any custom
|
||||||
|
# attributes updated here are written to HA within the same cycle.
|
||||||
|
# Only external managers are refreshed here: internal managers have their
|
||||||
|
# own dedicated per-cycle mechanisms. A per-manager guard prevents a
|
||||||
|
# faulty plugin from breaking the control loop.
|
||||||
|
for manager in self._external_managers:
|
||||||
|
try:
|
||||||
|
await manager.refresh_state()
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
_LOGGER.error(
|
||||||
|
"%s - Error while refreshing external feature manager '%s': %s",
|
||||||
|
self,
|
||||||
|
manager.name,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
# Check for heating/cooling failures (only for TPI VTherms)
|
# Check for heating/cooling failures (only for TPI VTherms)
|
||||||
await self._heating_failure_detection_manager.refresh_state()
|
await self._heating_failure_detection_manager.refresh_state()
|
||||||
|
|
||||||
@@ -1884,7 +1985,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
self._state_manager.add_custom_attributes(self._attr_extra_state_attributes)
|
self._state_manager.add_custom_attributes(self._attr_extra_state_attributes)
|
||||||
|
|
||||||
for manager in self._managers:
|
for manager in self._managers:
|
||||||
manager.add_custom_attributes(self._attr_extra_state_attributes)
|
# add_custom_attributes is optional for external feature managers
|
||||||
|
# (it is not part of the InterfaceFeatureManager contract), so call
|
||||||
|
# it defensively.
|
||||||
|
publish_attributes = getattr(manager, "add_custom_attributes", None)
|
||||||
|
if callable(publish_attributes):
|
||||||
|
publish_attributes(self._attr_extra_state_attributes)
|
||||||
|
|
||||||
def send_event(self, event_type: EventType, data: dict):
|
def send_event(self, event_type: EventType, data: dict):
|
||||||
"""Send an event"""
|
"""Send an event"""
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ async def async_setup_entry(
|
|||||||
["None", "Low", "Medium", "High", "Turbo"]
|
["None", "Low", "Medium", "High", "Turbo"]
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"service_set_auto_fan_mode",
|
"service_set_auto_fan_mode_deprecated",
|
||||||
)
|
)
|
||||||
|
|
||||||
platform.async_register_entity_service(
|
platform.async_register_entity_service(
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"numpy",
|
"numpy",
|
||||||
"scipy",
|
"scipy",
|
||||||
"vtherm_api>=0.3.0"
|
"vtherm_api>=0.4.0"
|
||||||
],
|
],
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"version": "10.1.0",
|
"version": "10.2.0",
|
||||||
"zeroconf": []
|
"zeroconf": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ set_auto_regulation_mode:
|
|||||||
|
|
||||||
set_auto_fan_mode:
|
set_auto_fan_mode:
|
||||||
name: Set Auto Fan mode
|
name: Set Auto Fan mode
|
||||||
description: Change the mode of auto-fan (only for VTherm over climate)
|
description: Deprecated when using the vtherm_auto_fan_extended plugin. Without the plugin, legacy core behavior is kept.
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
integration: versatile_thermostat
|
integration: versatile_thermostat
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from datetime import timedelta, datetime
|
|||||||
|
|
||||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval, EventStateChangedData, async_call_later
|
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval, EventStateChangedData, async_call_later
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
HVACAction,
|
HVACAction,
|
||||||
@@ -27,6 +28,9 @@ from .vtherm_hvac_mode import VThermHvacMode
|
|||||||
|
|
||||||
_LOGGER = get_vtherm_logger(__name__)
|
_LOGGER = get_vtherm_logger(__name__)
|
||||||
|
|
||||||
|
AUTO_FAN_PLUGIN_DOMAIN = "vtherm_auto_fan_extended"
|
||||||
|
AUTO_FAN_PLUGIN_TARGET_VTHERM_KEY = "target_vtherm_unique_id"
|
||||||
|
|
||||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||||
HVACAction.COOLING,
|
HVACAction.COOLING,
|
||||||
HVACAction.DRYING,
|
HVACAction.DRYING,
|
||||||
@@ -465,8 +469,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def determine_fan_mode_contains_speed(fan_modes: list[str]) -> bool:
|
def determine_fan_mode_contains_speed(fan_modes: list[str]) -> bool:
|
||||||
"""Determine if the fan_modes contains speed modes by searching for the keywords "low"/"1"/"one"/"speed_1"."""
|
"""Determine if the fan_modes contains speed modes by searching for the keywords "low"/"1"/"one"/"speed_1"/"on_low"."""
|
||||||
for val in ["low", "1", "one", "speed_1"]:
|
for val in ["low", "1", "one", "speed_1", "on_low"]:
|
||||||
if find_fan_mode(fan_modes, val):
|
if find_fan_mode(fan_modes, val):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -482,6 +486,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
index = speed_modes.index("one")
|
index = speed_modes.index("one")
|
||||||
elif "speed_1" in speed_modes:
|
elif "speed_1" in speed_modes:
|
||||||
index = speed_modes.index("speed_1")
|
index = speed_modes.index("speed_1")
|
||||||
|
elif "on_low" in speed_modes:
|
||||||
|
index = speed_modes.index("on_low")
|
||||||
|
|
||||||
if index > -1 and index >= len(speed_modes) / 2:
|
if index > -1 and index >= len(speed_modes) / 2:
|
||||||
speed_modes.reverse()
|
speed_modes.reverse()
|
||||||
@@ -1008,6 +1014,20 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
"""Get the regulated target temperature"""
|
"""Get the regulated target temperature"""
|
||||||
return self._regulated_target_temp
|
return self._regulated_target_temp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def regulated_target_temperature(self) -> float | None:
|
||||||
|
"""Return the regulated target temperature used to drive the underlying."""
|
||||||
|
return self._regulated_target_temp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def underlying_fan_modes(self) -> list[str] | None:
|
||||||
|
"""Return the fan modes exposed by the underlying climate(s)."""
|
||||||
|
return self.fan_modes
|
||||||
|
|
||||||
|
async def async_set_underlying_fan_mode(self, fan_mode: str) -> None:
|
||||||
|
"""Send a fan mode to the underlying climate(s)."""
|
||||||
|
await self.async_set_fan_mode(fan_mode)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_regulated(self) -> bool:
|
def is_regulated(self) -> bool:
|
||||||
"""Check if the ThermostatOverClimate is regulated"""
|
"""Check if the ThermostatOverClimate is regulated"""
|
||||||
@@ -1306,6 +1326,49 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
|||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def service_set_auto_fan_mode_deprecated(self, auto_fan_mode: str):
|
||||||
|
"""Deprecated service handler for auto fan mode.
|
||||||
|
|
||||||
|
Auto fan is now managed by the vtherm_auto_fan_extended plugin.
|
||||||
|
"""
|
||||||
|
config_entries = getattr(self._hass, "config_entries", None)
|
||||||
|
plugin_is_configured = False
|
||||||
|
if config_entries is not None:
|
||||||
|
try:
|
||||||
|
plugin_entries = config_entries.async_entries(AUTO_FAN_PLUGIN_DOMAIN)
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
plugin_entries = []
|
||||||
|
|
||||||
|
# If at least one entry targets this VTherm, plugin takes ownership.
|
||||||
|
plugin_is_configured = any(
|
||||||
|
entry.data.get(AUTO_FAN_PLUGIN_TARGET_VTHERM_KEY) == self.unique_id
|
||||||
|
for entry in plugin_entries
|
||||||
|
)
|
||||||
|
|
||||||
|
# Backward compatibility: old entries without target still mean plugin ownership.
|
||||||
|
if not plugin_is_configured and plugin_entries:
|
||||||
|
plugin_is_configured = any(
|
||||||
|
AUTO_FAN_PLUGIN_TARGET_VTHERM_KEY not in entry.data
|
||||||
|
for entry in plugin_entries
|
||||||
|
)
|
||||||
|
|
||||||
|
# If plugin is not configured for this VTherm, keep legacy core behavior.
|
||||||
|
if not plugin_is_configured:
|
||||||
|
await self.service_set_auto_fan_mode(auto_fan_mode)
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.warning(
|
||||||
|
"%s - set_auto_fan_mode is deprecated in core and managed by the "
|
||||||
|
"vtherm_auto_fan_extended plugin (requested: %s)",
|
||||||
|
self,
|
||||||
|
auto_fan_mode,
|
||||||
|
)
|
||||||
|
raise HomeAssistantError(
|
||||||
|
"The set_auto_fan_mode service is now managed by the "
|
||||||
|
"vtherm_auto_fan_extended plugin and is no longer available in "
|
||||||
|
"Versatile Thermostat core."
|
||||||
|
)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
async def async_turn_off(self) -> None:
|
async def async_turn_off(self) -> None:
|
||||||
"""Turn off the climate entity."""
|
"""Turn off the climate entity."""
|
||||||
|
|||||||
Reference in New Issue
Block a user