Skip to content

Commit

Permalink
Merge pull request #79 from basbruss/temp_switch
Browse files Browse the repository at this point in the history
Add outdoor temperature control
  • Loading branch information
basbruss authored Mar 21, 2024
2 parents ad55342 + 56868e3 commit 2f48fb5
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 54 deletions.
46 changes: 33 additions & 13 deletions custom_components/adaptive_cover/calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
import numpy as np
import pandas as pd
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import state_attr
from numpy import cos, sin, tan
from numpy import radians as rad

from .helpers import get_domain, get_safe_attribute, get_safe_state
from .helpers import get_domain, get_safe_state
from .sun import SunData


Expand Down Expand Up @@ -147,20 +148,39 @@ class ClimateCoverData:
presence_entity: str
weather_entity: str
weather_condition: list[str]
outside_entity: str
temp_switch: bool
blind_type: str

@property
def current_temperature(self) -> float:
"""Get current temp from entity."""
def outside_temperature(self):
"""Get outside temperature."""
temp = None
if self.weather_entity:
temp = state_attr(self.hass, self.weather_entity, "temperature")
if self.outside_entity:
temp = get_safe_state(self.outside_entity, self.hass)
return temp

@property
def inside_temperature(self):
"""Get inside temp from entity."""
if self.temp_entity is not None:
if get_domain(self.temp_entity) != "climate":
temp = get_safe_state(self.hass, self.temp_entity)
temp = get_safe_state(self.temp_entity, self.hass)
else:
temp = get_safe_attribute(
self.hass, self.temp_entity, "current_temperature"
)
temp = state_attr(self.hass, self.temp_entity, "current_temperature")
return temp

@property
def get_current_temperature(self) -> float:
"""Get temperature."""
if self.temp_switch:
if self.outside_temperature:
return float(self.outside_temperature)
if self.inside_temperature:
return float(self.inside_temperature)

@property
def is_presence(self):
"""Checks if people are present."""
Expand All @@ -181,23 +201,23 @@ def is_presence(self):
@property
def is_winter(self) -> bool:
"""Check if temperature is below threshold."""
if self.temp_low is not None and self.current_temperature is not None:
return float(self.current_temperature) < self.temp_low
if self.temp_low is not None and self.get_current_temperature is not None:
return self.get_current_temperature < self.temp_low
return False

@property
def is_summer(self) -> bool:
"""Check if temperature is over threshold."""
if self.temp_high is not None and self.current_temperature is not None:
return float(self.current_temperature) > self.temp_high
if self.temp_high is not None and self.get_current_temperature is not None:
return self.get_current_temperature > self.temp_high
return False

@property
def is_sunny(self) -> bool:
"""Check if condition can contain radiation in winter."""
weather_state = None
if self.weather_entity is not None:
weather_state = get_safe_state(self.hass, self.weather_entity)
weather_state = get_safe_state(self.weather_entity, self.hass)
if self.weather_condition is not None:
return weather_state in self.weather_condition
return True
Expand Down Expand Up @@ -272,7 +292,7 @@ def control_method_tilt_bi(self):
def tilt_state(self):
"""Add tilt specific controls."""
if (
self.climate_data.current_temperature is not None
self.climate_data.get_current_temperature is not None
and self.cover.sol_elev > 0
):
if self.cover.mode == "mode1":
Expand Down
31 changes: 27 additions & 4 deletions custom_components/adaptive_cover/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
CONF_LENGTH_AWNING,
CONF_MAX_POSITION,
CONF_MODE,
CONF_OUTSIDETEMP_ENTITY,
CONF_PRESENCE_ENTITY,
CONF_SENSOR_TYPE,
CONF_SUNSET_OFFSET,
Expand Down Expand Up @@ -148,21 +149,30 @@

CLIMATE_OPTIONS = vol.Schema(
{
vol.Required(CONF_TEMP_ENTITY): selector.EntitySelector(
selector.EntityFilterSelectorConfig(domain=["climate", "sensor"])
),
vol.Required(CONF_TEMP_LOW, default=21): selector.NumberSelector(
selector.NumberSelectorConfig(min=0, max=86, step=1, mode="slider")
),
vol.Required(CONF_TEMP_HIGH, default=25): selector.NumberSelector(
selector.NumberSelectorConfig(min=0, max=90, step=1, mode="slider")
),
vol.Required(CONF_TEMP_ENTITY): selector.EntitySelector(
selector.EntityFilterSelectorConfig(domain=["climate", "sensor"])
vol.Optional(
CONF_OUTSIDETEMP_ENTITY, default=vol.UNDEFINED
): selector.EntitySelector(
selector.EntityFilterSelectorConfig(domain=["sensor"])
),
vol.Optional(CONF_PRESENCE_ENTITY): selector.EntitySelector(
vol.Optional(
CONF_PRESENCE_ENTITY, default=vol.UNDEFINED
): selector.EntitySelector(
selector.EntityFilterSelectorConfig(
domain=["device_tracker", "zone", "binary_sensor", "input_boolean"]
)
),
vol.Optional(CONF_WEATHER_ENTITY): selector.EntitySelector(
vol.Optional(
CONF_WEATHER_ENTITY, default=vol.UNDEFINED
): selector.EntitySelector(
selector.EntityFilterSelectorConfig(domain="weather")
),
}
Expand Down Expand Up @@ -319,6 +329,7 @@ async def async_step_update(self, user_input: dict[str, Any] | None = None):
CONF_WEATHER_ENTITY: self.config.get(CONF_WEATHER_ENTITY),
CONF_TEMP_LOW: self.config.get(CONF_TEMP_LOW),
CONF_TEMP_HIGH: self.config.get(CONF_TEMP_HIGH),
CONF_OUTSIDETEMP_ENTITY: self.config.get(CONF_OUTSIDETEMP_ENTITY),
CONF_CLIMATE_MODE: self.config.get(CONF_CLIMATE_MODE),
CONF_WEATHER_STATE: self.config.get(CONF_WEATHER_STATE),
},
Expand Down Expand Up @@ -406,6 +417,12 @@ async def async_step_tilt(self, user_input: dict[str, Any] | None = None):
async def async_step_climate(self, user_input: dict[str, Any] | None = None):
"""Manage climate options."""
if user_input is not None:
entities = [
CONF_OUTSIDETEMP_ENTITY,
CONF_WEATHER_ENTITY,
CONF_PRESENCE_ENTITY,
]
self.optional_entities(entities, user_input)
self.options.update(user_input)
if self.options.get(CONF_WEATHER_ENTITY):
return await self.async_step_weather()
Expand All @@ -432,3 +449,9 @@ async def async_step_weather(self, user_input: dict[str, Any] | None = None):
async def _update_options(self) -> FlowResult:
"""Update config entry options."""
return self.async_create_entry(title="", data=self.options)

def optional_entities(self, keys: list, user_input: dict[str, Any] | None = None):
"""Set value to None if key does not exist."""
for key in keys:
if key not in user_input:
user_input[key] = None
1 change: 1 addition & 0 deletions custom_components/adaptive_cover/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
CONF_CLIMATE_MODE = "climate_mode"
CONF_WEATHER_STATE = "weather_state"
CONF_MAX_POSITION = "max_position"
CONF_OUTSIDETEMP_ENTITY = "outside_temp"

STRATEGY_MODE_BASIC = "basic"
STRATEGY_MODE_CLIMATE = "climate"
Expand Down
32 changes: 26 additions & 6 deletions custom_components/adaptive_cover/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.template import state_attr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .calculation import (
Expand All @@ -29,6 +30,7 @@
CONF_INVERSE_STATE,
CONF_LENGTH_AWNING,
CONF_MAX_POSITION,
CONF_OUTSIDETEMP_ENTITY,
CONF_PRESENCE_ENTITY,
CONF_SUNSET_OFFSET,
CONF_SUNSET_POS,
Expand All @@ -43,7 +45,6 @@
DOMAIN,
LOGGER,
)
from .helpers import get_safe_attribute


@dataclass
Expand Down Expand Up @@ -76,6 +77,7 @@ def __init__(self, hass: HomeAssistant) -> None: # noqa: D107
self._climate_mode = self.config_entry.options.get(CONF_CLIMATE_MODE, False)
self._switch_mode = True if self._climate_mode else False
self._inverse_state = self.config_entry.options.get(CONF_INVERSE_STATE, False)
self._temp_toggle = False

async def async_check_entity_state_change(
self, entity: str, old_state: State | None, new_state: State | None
Expand All @@ -86,8 +88,8 @@ async def async_check_entity_state_change(

async def _async_update_data(self) -> AdaptiveCoverData:
pos_sun = [
get_safe_attribute(self.hass, "sun.sun", "azimuth"),
get_safe_attribute(self.hass, "sun.sun", "elevation"),
state_attr(self.hass, "sun.sun", "azimuth"),
state_attr(self.hass, "sun.sun", "elevation"),
]

common_data = [
Expand Down Expand Up @@ -142,6 +144,8 @@ async def _async_update_data(self) -> AdaptiveCoverData:
self.config_entry.options.get(CONF_PRESENCE_ENTITY),
self.config_entry.options.get(CONF_WEATHER_ENTITY),
self.config_entry.options.get(CONF_WEATHER_STATE),
self.config_entry.options.get(CONF_OUTSIDETEMP_ENTITY),
self._temp_toggle,
self._cover_type,
]
climate = ClimateCoverData(*climate_data_var)
Expand All @@ -154,16 +158,19 @@ async def _async_update_data(self) -> AdaptiveCoverData:

default_state = round(NormalCoverState(cover_data).get_state())

state = default_state
if self._switch_mode:
state = climate_state

if self._inverse_state:
default_state = 100 - default_state
if self._climate_mode:
climate_state = 100 - climate_state
state = 100 - state

return AdaptiveCoverData(
climate_mode_toggle=self.switch_mode,
states={
"normal": default_state,
"climate": climate_state,
"state": state,
"start": NormalCoverState(cover_data).cover.solar_times()[0],
"end": NormalCoverState(cover_data).cover.solar_times()[1],
"control": control_method,
Expand All @@ -180,6 +187,10 @@ async def _async_update_data(self) -> AdaptiveCoverData:
],
"entity_id": self.config_entry.options.get(CONF_ENTITIES),
"cover_type": self._cover_type,
"outside": self.config_entry.options.get(CONF_OUTSIDETEMP_ENTITY),
"outside_temp": climate_data.outside_temperature,
"current_temp": climate_data.get_current_temperature,
"toggle": climate_data.temp_switch,
},
)

Expand All @@ -191,3 +202,12 @@ def switch_mode(self):
@switch_mode.setter
def switch_mode(self, value):
self._switch_mode = value

@property
def temp_toggle(self):
"""Let switch toggle climate mode."""
return self._temp_toggle

@temp_toggle.setter
def temp_toggle(self, value):
self._temp_toggle = value
14 changes: 2 additions & 12 deletions custom_components/adaptive_cover/helpers.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
"""Helper functions."""

from homeassistant.core import split_entity_id
from homeassistant.core import HomeAssistant, split_entity_id


def get_safe_state(hass, entity_id: str):
def get_safe_state(entity_id: str, hass: HomeAssistant):
"""Get a safe state value if not available."""
state = hass.states.get(entity_id)
if not state or state.state in ["unknown", "unavailable"]:
return None
return state.state


def get_safe_attribute(hass, entity_id: str, attribute: str):
"""Get a safe value from attribute."""
if not get_safe_state(hass, entity_id):
return None
attr_obj = hass.states.get(entity_id).attributes
if attribute not in attr_obj:
return None
return attr_obj[attribute]


def get_domain(entity: str):
"""Get domain of entity."""
if entity is not None:
Expand Down
4 changes: 1 addition & 3 deletions custom_components/adaptive_cover/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,7 @@ def name(self):
@property
def native_value(self) -> str | None:
"""Handle when entity is added."""
if self.data.climate_mode_toggle:
return self.data.states["climate"]
return self.data.states["normal"]
return self.data.states["state"]

@property
def device_info(self) -> DeviceInfo:
Expand Down
8 changes: 6 additions & 2 deletions custom_components/adaptive_cover/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@
"temp_entity": "Inside temperature entity",
"presence_entity": "Presence entity",
"weather_entity": "Weather entity (optional)",
"outside_temp": "Outside temperature sensor",
"temp_low": "Low temperature threshold",
"temp_high": "High temperature threshold"
},
"data_description":{
"presence_entity": "Entity to represent the presence status in the room or home",
"weather_entity": "Checks for weather conditions",
"weather_entity": "Checks for weather conditions and can be used for the outside temperature",
"outside_temp":"This entity overrides the outside temperature from the weather entity if both are set",
"temp_low": "The minimum comfort temperature",
"temp_high": "The maximum comfort temperature"
},
Expand Down Expand Up @@ -177,12 +179,14 @@
"temp_entity": "Inside temperature entity",
"presence_entity": "Presence entity",
"weather_entity": "Weather entity (optional)",
"outside_temp": "Outside temperature sensor",
"temp_low": "Low temperature threshold",
"temp_high": "High temperature threshold"
},
"data_description":{
"presence_entity": "Entity to represent the presence status in the room or home",
"weather_entity": "Checks for weather conditions",
"weather_entity": "Checks for weather conditions and can be used for the outside temperature",
"outside_temp":"This entity overrides the outside temperature from the weather entity if both are set",
"temp_low": "The minimum comfort temperature",
"temp_high": "The maximum comfort temperature"
},
Expand Down
Loading

0 comments on commit 2f48fb5

Please sign in to comment.