Skip to content

Commit

Permalink
Merge pull request #212 from basbruss/interpolation-shutters
Browse files Browse the repository at this point in the history
Interpolation shutters
  • Loading branch information
basbruss authored Jun 14, 2024
2 parents 6c80833 + 1572d62 commit e290b30
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 24 deletions.
79 changes: 79 additions & 0 deletions custom_components/adaptive_cover/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
CONF_FOV_LEFT,
CONF_FOV_RIGHT,
CONF_HEIGHT_WIN,
CONF_INTERP,
CONF_INTERP_END,
CONF_INTERP_LIST,
CONF_INTERP_LIST_NEW,
CONF_INTERP_START,
CONF_INVERSE_STATE,
CONF_IRRADIANCE_ENTITY,
CONF_IRRADIANCE_THRESHOLD,
Expand Down Expand Up @@ -136,6 +141,7 @@
),
vol.Required(CONF_INVERSE_STATE, default=False): bool,
vol.Required(CONF_ENABLE_BLIND_SPOT, default=False): bool,
vol.Required(CONF_INTERP, default=False): bool,
}
)

Expand Down Expand Up @@ -326,6 +332,27 @@
}
)

INTERPOLATION_OPTIONS = vol.Schema(
{
vol.Optional(CONF_INTERP_START): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
),
vol.Optional(CONF_INTERP_END): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
),
vol.Optional(CONF_INTERP_LIST, default=[]): selector.SelectSelector(
selector.SelectSelectorConfig(
multiple=True, custom_value=True, options=["0", "50", "100"]
)
),
vol.Optional(CONF_INTERP_LIST_NEW, default=[]): selector.SelectSelector(
selector.SelectSelectorConfig(
multiple=True, custom_value=True, options=["0", "50", "100"]
)
),
}
)


def _get_azimuth_edges(data) -> tuple[int, int]:
"""Calculate azimuth edges."""
Expand Down Expand Up @@ -377,6 +404,8 @@ async def async_step_vertical(self, user_input: dict[str, Any] | None = None):
},
)
self.config.update(user_input)
if self.config[CONF_INTERP]:
return await self.async_step_interp()
if self.config[CONF_ENABLE_BLIND_SPOT]:
return await self.async_step_blind_spot()
return await self.async_step_automation()
Expand All @@ -402,6 +431,8 @@ async def async_step_horizontal(self, user_input: dict[str, Any] | None = None):
},
)
self.config.update(user_input)
if self.config[CONF_INTERP]:
return await self.async_step_interp()
if self.config[CONF_ENABLE_BLIND_SPOT]:
return await self.async_step_blind_spot()
return await self.async_step_automation()
Expand All @@ -427,13 +458,34 @@ async def async_step_tilt(self, user_input: dict[str, Any] | None = None):
},
)
self.config.update(user_input)
if self.config[CONF_INTERP]:
return await self.async_step_interp()
if self.config[CONF_ENABLE_BLIND_SPOT]:
return await self.async_step_blind_spot()
return await self.async_step_automation()
return self.async_show_form(
step_id="tilt", data_schema=CLIMATE_MODE.extend(TILT_OPTIONS.schema)
)

async def async_step_interp(self, user_input: dict[str, Any] | None = None):
"""Show interpolation options."""
if user_input is not None:
if len(user_input[CONF_INTERP_LIST]) != len(
user_input[CONF_INTERP_LIST_NEW]
):
return self.async_show_form(
step_id="interp",
data_schema=INTERPOLATION_OPTIONS,
errors={
CONF_INTERP_LIST_NEW: "Must have same length as 'Interpolation' list"
},
)
self.config.update(user_input)
if self.config[CONF_ENABLE_BLIND_SPOT]:
return await self.async_step_blind_spot()
return await self.async_step_automation()
return self.async_show_form(step_id="interp", data_schema=INTERPOLATION_OPTIONS)

async def async_step_blind_spot(self, user_input: dict[str, Any] | None = None):
"""Add blindspot to data."""
edges = _get_azimuth_edges(self.config)
Expand Down Expand Up @@ -554,10 +606,16 @@ async def async_step_update(self, user_input: dict[str, Any] | None = None):
CONF_MIN_ELEVATION: self.config.get(CONF_MIN_ELEVATION, None),
CONF_MAX_ELEVATION: self.config.get(CONF_MAX_ELEVATION, None),
CONF_TRANSPARENT_BLIND: self.config.get(CONF_TRANSPARENT_BLIND, False),
CONF_INTERP_START: self.config.get(CONF_INTERP_START, None),
CONF_INTERP_END: self.config.get(CONF_INTERP_END, None),
CONF_INTERP_LIST: self.config.get(CONF_INTERP_LIST, []),
CONF_INTERP_LIST_NEW: self.config.get(CONF_INTERP_LIST_NEW, []),
CONF_INTERP: self.config.get(CONF_INTERP),
CONF_LUX_ENTITY: self.config.get(CONF_LUX_ENTITY),
CONF_LUX_THRESHOLD: self.config.get(CONF_LUX_THRESHOLD),
CONF_IRRADIANCE_ENTITY: self.config.get(CONF_IRRADIANCE_ENTITY),
CONF_IRRADIANCE_THRESHOLD: self.config.get(CONF_IRRADIANCE_THRESHOLD),

},
)

Expand Down Expand Up @@ -586,6 +644,8 @@ async def async_step_init(
options.append("weather")
if self.options.get(CONF_ENABLE_BLIND_SPOT):
options.append("blind_spot")
if self.options.get(CONF_INTERP):
options.append("interp")
return self.async_show_menu(step_id="init", menu_options=options)

async def async_step_automation(self, user_input: dict[str, Any] | None = None):
Expand Down Expand Up @@ -716,6 +776,25 @@ async def async_step_tilt(self, user_input: dict[str, Any] | None = None):
),
)

async def async_step_interp(self, user_input: dict[str, Any] | None = None):
"""Show interpolation options."""
if user_input is not None:
if len(user_input[CONF_INTERP_LIST]) != len(
user_input[CONF_INTERP_LIST_NEW]
):
return self.async_show_form(
step_id="interp",
data_schema=INTERPOLATION_OPTIONS,
errors={
CONF_INTERP_LIST_NEW: "Must have same length as 'Interpolation' list"
},
)
self.options.update(user_input)
return await self._update_options()
return self.add_suggested_values_to_schema(
INTERPOLATION_OPTIONS, user_input or self.options
)

async def async_step_blind_spot(self, user_input: dict[str, Any] | None = None):
"""Add blindspot to data."""
edges = _get_azimuth_edges(self.options)
Expand Down
5 changes: 5 additions & 0 deletions custom_components/adaptive_cover/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
CONF_MIN_ELEVATION = "min_elevation"
CONF_MAX_ELEVATION = "max_elevation"
CONF_TRANSPARENT_BLIND = "transparent_blind"
CONF_INTERP_START = "interp_start"
CONF_INTERP_END = "interp_end"
CONF_INTERP_LIST = "interp_list"
CONF_INTERP_LIST_NEW = "interp_list_new"
CONF_INTERP = "interp"
CONF_LUX_ENTITY = "lux_entity"
CONF_LUX_THRESHOLD = "lux_threshold"
CONF_IRRADIANCE_ENTITY = "irradiance_entity"
Expand Down
43 changes: 40 additions & 3 deletions custom_components/adaptive_cover/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import datetime as dt
from dataclasses import dataclass

import numpy as np
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
Expand Down Expand Up @@ -46,6 +47,11 @@
CONF_FOV_LEFT,
CONF_FOV_RIGHT,
CONF_HEIGHT_WIN,
CONF_INTERP,
CONF_INTERP_END,
CONF_INTERP_LIST,
CONF_INTERP_LIST_NEW,
CONF_INTERP_START,
CONF_INVERSE_STATE,
CONF_IRRADIANCE_ENTITY,
CONF_IRRADIANCE_THRESHOLD,
Expand Down Expand Up @@ -110,7 +116,9 @@ def __init__(self, hass: HomeAssistant) -> None: # noqa: D107
self._cover_type = self.config_entry.data.get("sensor_type")
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._inverse_state = self.config_entry.options.get
(CONF_INVERSE_STATE, False)
self._use_interpolation = self.config_entry.options.get(CONF_INTERP, False)
self._temp_toggle = None
self._control_toggle = None
self._manual_toggle = None
Expand Down Expand Up @@ -235,7 +243,7 @@ async def _async_update_data(self) -> AdaptiveCoverData:
if self.timed_refresh:
await self.async_handle_timed_refresh(options)

normal_cover = normal_cover_state.cover
normal_cover = self.normal_cover_state.cover
# Run the solar_times method in a separate thread
loop = asyncio.get_event_loop()
start, end = await loop.run_in_executor(None, normal_cover.solar_times)
Expand Down Expand Up @@ -363,6 +371,10 @@ def _update_options(self, options):
CONF_MANUAL_OVERRIDE_DURATION, {"minutes": 15}
)
self.manual_threshold = options.get(CONF_MANUAL_THRESHOLD)
self.start_value = options.get(CONF_INTERP_START)
self.end_value = options.get(CONF_INTERP_END)
self.normal_list = options.get(CONF_INTERP_LIST)
self.new_list = options.get(CONF_INTERP_LIST_NEW)

def _update_manager_and_covers(self):
self.manager.add_covers(self.entities)
Expand Down Expand Up @@ -576,11 +588,36 @@ def state(self) -> int:
state = self.default_state
if self._switch_mode:
state = self.climate_state
if self._inverse_state:

if self._use_interpolation:
state = self.interpolate_states(state)

if self._inverse_state and self._use_interpolation:
_LOGGER.info(
"Inverse state is not supported with interpolation, you can inverse the state by arranging the list from high to low"
)

if self._inverse_state and not self._use_interpolation:
state = inverse_state(state)

_LOGGER.debug("Calculated position: %s", state)
return state

def interpolate_states(self, state):
"""Interpolate states."""
normal_range = [0, 100]
if self.start_value and self.end_value:
new_range = [self.start_value, self.end_value]
if self.normal_list and self.new_list:
normal_range = list(map(int, self.normal_list))
new_range = list(map(int, self.new_list))
state = np.interp(state, normal_range, new_range)
if state == new_range[0]:
state = 0
if state == new_range[-1]:
state = 100
return state

@property
def switch_mode(self):
"""Let switch toggle climate mode."""
Expand Down
49 changes: 42 additions & 7 deletions custom_components/adaptive_cover/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"climate_mode": "Climate Mode",
"blind_spot": "Setup Blindspot",
"min_elevation": "Minimum Elevation of the sun",
"max_elevation": "Maximum Elevation of the sun"
"max_elevation": "Maximum Elevation of the sun",
"interp": "Set custom open/close positions for My Cover, if it doesn't fully operate at 0-100%"
},
"data_description": {
"set_azimuth": "Adjust Azimuth",
Expand Down Expand Up @@ -125,7 +126,8 @@
"climate_mode": "Climate Mode",
"blind_spot": "Setup Blindspot",
"min_elevation": "Minimum Elevation of the sun",
"max_elevation": "Maximum Elevation of the sun"
"max_elevation": "Maximum Elevation of the sun",
"interp": "Set custom open/close positions for My Cover, if it doesn't fully operate at 0-100%"
},
"data_description": {
"set_azimuth": "Specify the Azimuth",
Expand Down Expand Up @@ -164,7 +166,8 @@
"tilt_mode": "Type of movement",
"blind_spot": "Setup Blindspot",
"min_elevation": "Minimum Elevation of the sun",
"max_elevation": "Maximum Elevation of the sun"
"max_elevation": "Maximum Elevation of the sun",
"interp": "Set custom open/close positions for My Cover, if it doesn't fully operate at 0-100%"
},
"data_description": {
"set_azimuth": "Specify the Azimuth",
Expand Down Expand Up @@ -197,6 +200,20 @@
},
"title": "Blindspot",
"description": "Setup blindspot for the cover. These values are relative degrees to the width of the field of view. Where 0 is the leftmost position taken from the field of view left parameter."
},
"interp": {
"data": {
"interp_start": "Value where opening starts",
"interp_end": "Value where closing starts",
"interp_list": "List of interpolation",
"interp_list_new": "Custom interpolation list"
},
"data_description": {
"interp_list": "List of interpolation values, where 0 is fully closed and 100 is fully open",
"interp_list_new": "Custom range of interpolation values, where the beginning should represent closed and the end open"
},
"title": "Range Adjustment",
"description": "Adjust the values at which your Cover effectively opens or closes. Ideal for Covers that don't fully open at 0% or close at 100%. \n The first two sliders are for a 2 point adjustment, the list options are for a multiple point adjustment"
}
}
},
Expand All @@ -208,7 +225,8 @@
"blind": "Fine-tune Blind Settings",
"climate": "Edit Climate Configuration",
"weather": "Edit Weather Configuration",
"blind_spot": "Setup Blindspot"
"blind_spot": "Setup Blindspot",
"interp": "Range Adjustment"
}
},
"automation": {
Expand Down Expand Up @@ -253,7 +271,8 @@
"climate_mode": "Climate Mode",
"blind_spot": "Setup Blindspot",
"min_elevation": "Minimum Elevation of the sun",
"max_elevation": "Maximum Elevation of the sun"
"max_elevation": "Maximum Elevation of the sun",
"interp": "Set custom open/close positions for My Cover, if it doesn't fully operate at 0-100%"
},
"data_description": {
"set_azimuth": "Adjust Azimuth",
Expand Down Expand Up @@ -327,7 +346,8 @@
"climate_mode": "Climate Mode",
"blind_spot": "Setup Blindspot",
"min_elevation": "Minimum Elevation of the sun",
"max_elevation": "Maximum Elevation of the sun"
"max_elevation": "Maximum Elevation of the sun",
"interp": "Set custom open/close positions for My Cover, if it doesn't fully operate at 0-100%"
},
"data_description": {
"set_azimuth": "Specify the Azimuth",
Expand Down Expand Up @@ -366,7 +386,8 @@
"tilt_mode": "Type of movement",
"blind_spot": "Setup Blindspot",
"min_elevation": "Minimum Elevation of the sun",
"max_elevation": "Maximum Elevation of the sun"
"max_elevation": "Maximum Elevation of the sun",
"interp": "Set custom open/close positions for My Cover, if it doesn't fully operate at 0-100%"
},
"data_description": {
"set_azimuth": "Specify the Azimuth",
Expand Down Expand Up @@ -399,6 +420,20 @@
},
"title": "Blindspot",
"description": "Setup blindspot for the cover. These values are relative degrees to the width of the field of view. Where 0 is the leftmost position taken from the field of view left parameter."
},
"interp": {
"data": {
"interp_start": "Value where opening starts",
"interp_end": "Value where closing starts",
"interp_list": "List of interpolation",
"interp_list_new": "Custom interpolation list"
},
"data_description": {
"interp_list": "List of interpolation values, where 0 is fully closed and 100 is fully open",
"interp_list_new": "Custom range of interpolation values, where the beginning should represent closed and the end open"
},
"title": "Range Adjustment",
"description": "Adjust the values at which your Cover effectively opens or closes. Ideal for Covers that don't fully open at 0% or close at 100%. \n The first two sliders are for a 2 point adjustment, the list options are for a multiple point adjustment"
}
}
},
Expand Down
Loading

0 comments on commit e290b30

Please sign in to comment.