working 2.0 clone

This commit is contained in:
root
2024-10-01 14:06:39 +00:00
commit 873064d680
704 changed files with 46961 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
#
# Copyright (c) 2022, Diogo Silva "Soloam"
# Creative Commons BY-NC-SA 4.0 International Public License
# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/)
#
"""
PID Controller.
For more details about this sensor, please refer to the documentation at
https://github.com/soloam/ha-pid-controller/
"""
import logging
from distutils import util
import homeassistant.helpers.config_validation as cv
from homeassistant.core import HomeAssistant
from homeassistant.helpers.service import verify_domain_control
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.exceptions import HomeAssistantError
import voluptuous as vol
# pylint: disable=wildcard-import, unused-wildcard-import
from .const import *
__version__ = VERSION
_LOGGER = logging.getLogger(__name__)
SERVICE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
}
)
# pylint: disable=unused-argument
async def async_setup(hass: HomeAssistant, config):
"""Set up a pid."""
_LOGGER.debug("setup")
async def async_pid_service_reset(call) -> None:
"""Call pid service handler."""
_LOGGER.info("%s service called", call.service)
await pid_reset_service(hass, call)
hass.services.async_register(
COMPONENT_DOMAIN,
SERVICE_RESET_PID,
async_pid_service_reset,
schema=SERVICE_SCHEMA,
)
async def async_pid_service_autotune(call) -> None:
"""Call pid service handler."""
_LOGGER.info("%s service called", call.service)
await pid_autotune_service(hass, call)
hass.services.async_register(
COMPONENT_DOMAIN,
SERVICE_AUTOTUNE,
async_pid_service_autotune,
schema=SERVICE_SCHEMA,
)
return True
def get_entity_from_domain(hass: HomeAssistant, domain, entity_id):
component = hass.data.get(domain)
if component is None:
raise HomeAssistantError(f"{domain} component not set up")
entity = component.get_entity(entity_id)
if entity is None:
raise HomeAssistantError(f"{entity_id} not found")
return entity
async def pid_reset_service(hass: HomeAssistant, call):
entity_id = call.data["entity_id"]
domain = entity_id.split(".")[0]
_LOGGER.info("%s reset pid", entity_id)
try:
get_entity_from_domain(hass, domain, entity_id).reset_pid()
except AttributeError:
raise HomeAssistantError(f"{entity_id} can't reset PID") from AttributeError
async def pid_autotune_service(hass: HomeAssistant, call):
entity_id = call.data["entity_id"]
domain = entity_id.split(".")[0]
_LOGGER.info("%s autotune pid", entity_id)
try:
get_entity_from_domain(hass, domain, entity_id).start_autotune()
except AttributeError:
raise HomeAssistantError(f"{entity_id} can't reset PID") from AttributeError

View File

@@ -0,0 +1,92 @@
#
# Copyright (c) 2022, Diogo Silva "Soloam"
# Creative Commons BY-NC-SA 4.0 International Public License
# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/)
#
"""
PID Controller.
For more details about this sensor, please refer to the documentation at
https://github.com/soloam/ha-pid-controller/
"""
# Data
COMPONENT_DOMAIN = "pid_controller"
VERSION = "1.0.0"
# Services
COMPONENT_SERVICES = "pid-services"
SERVICE_RESET_PID = "reset_pid"
SERVICE_AUTOTUNE = "autotune_pid"
# Configuration
CONF_SETPOINT = "set_point"
CONF_PROPORTIONAL = "p"
CONF_INTEGRAL = "i"
CONF_DERIVATIVE = "d"
CONF_INVERT = "invert"
CONF_PRECISION = "precision"
CONF_ROUND = "round"
CONF_SAMPLE_TIME = "sample_time"
CONF_WINDUP = "windup"
CONF_ENABLED = "enabled"
# Default
DEFAULT_NAME = "PID Controller"
DEFAULT_PRECISION = 2
DEFAULT_MINIMUM = 0
DEFAULT_MAXIMUM = 1
DEFAULT_ROUND = "round"
DEFAULT_SAMPLE_TIME = 0
DEFAULT_WINDUP = 20
DEFAULT_UNIT_OF_MEASUREMENT = "points"
DEFAULT_DEVICE_CLASS = "None"
DEFAULT_ICON = "mdi:chart-bell-curve-cumulative"
DEFAULT_ENABLED = True
# Other
ROUND_FLOOR = "floor"
ROUND_CEIL = "ceil"
ROUND_ROUND = "round"
# Attributes
ATTR_ENABLED = "enabled"
ATTR_TUNNING = "tunning"
ATTR_ICON = "icon"
ATTR_UNIT_OF_MEASUREMENT = "unit_of_measuremnt"
ATTR_DEVICE_CLASS = "device_class"
ATTR_PROPORTIONAL = "proportional"
ATTR_INTEGRAL = "integral"
ATTR_DERIVATIVE = "derivative"
ATTR_SETPOINT = "set_point"
ATTR_SOURCE = "source"
ATTR_PRECISION = "precision"
ATTR_MINIMUM = "minimum"
ATTR_MAXIMUM = "maximum"
ATTR_RAW_STATE = "raw_state"
ATTR_SAMPLE_TIME = "sample_time"
ATTR_INVERT = "invert"
ATTR_P = "p"
ATTR_I = "i"
ATTR_D = "d"
ATTR_TO_PROPERTY = [
ATTR_ENABLED,
ATTR_TUNNING,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
ATTR_DEVICE_CLASS,
ATTR_PROPORTIONAL,
ATTR_INTEGRAL,
ATTR_DERIVATIVE,
ATTR_SETPOINT,
ATTR_SOURCE,
ATTR_PRECISION,
ATTR_MINIMUM,
ATTR_MAXIMUM,
ATTR_RAW_STATE,
ATTR_INVERT,
ATTR_SAMPLE_TIME,
ATTR_P,
ATTR_I,
ATTR_D,
]

View File

@@ -0,0 +1,16 @@
{
"domain": "pid_controller",
"name": "PID Controller",
"version": "v0.0.0",
"documentation": "https://github.com/soloam/ha-pid-controller/",
"issue_tracker": "https://github.com/soloam/ha-pid-controller/issues",
"dependencies": [],
"after_dependencies": [
],
"config_flow": false,
"codeowners": [
"@Soloam"
],
"requirements": [],
"iot_class": "calculated"
}

View File

@@ -0,0 +1,213 @@
#
# Copyright (c) 2022, Diogo Silva "Soloam"
# Creative Commons BY-NC-SA 4.0 International Public License
# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/)
#
"""
PID Controller.
For more details about this sensor, please refer to the documentation at
https://github.com/soloam/ha-pid-controller/
"""
import time
# pylint: disable=invalid-name
class PIDController:
"""PID Controller"""
WARMUP_STAGE = 3
def __init__(self, P=0.2, I=0.0, D=0.0, logger=None):
self._logger = logger
self._set_point = 0
self._windup = (None, None)
self._output = 0.0
self._kp = P
self._ki = I
self._kd = D
self._p_term = 0.0
self._i_term = 0.0
self._d_term = 0.0
self._sample_time = None
self._last_output = None
self._last_input = None
self._last_time = None
self.reset_pid()
def reset_pid(self):
self._p_term = 0.0
self._i_term = 0.0
self._d_term = 0.0
self._sample_time = None
self._last_output = None
self._last_input = None
self._last_time = None
def update(self, feedback_value, in_time=None):
"""Calculates PID value for given reference feedback"""
current_time = in_time if in_time is not None else self.current_time()
if self._last_time is None:
self._last_time = current_time
# Fill PID information
delta_time = current_time - self._last_time
if not delta_time:
delta_time = 1e-16
elif delta_time < 0:
return
# Return last output if sample time not met
if (
self._sample_time is not None
and self._last_output is not None
and delta_time < self._sample_time
):
return self._last_output
# Calculate error
error = self._set_point - feedback_value
last_error = self._set_point - (
self._last_input if self._last_input is not None else self._set_point
)
# Calculate delta error
delta_error = error - last_error
# Calculate P
self._p_term = self._kp * error
# Calculate I and avoids Sturation
if self._last_output is None or (
self._last_output > 0 and self._last_output < 100
):
self._i_term += self._ki * error * delta_time
self._i_term = self.clamp_value(self._i_term, self._windup)
# Calculate D
self._d_term = self._kd * delta_error / delta_time
# Compute final output
self._output = self._p_term + self._i_term + self._d_term
self._output = self.clamp_value(self._output, (0, 100))
# Keep Track
self._last_output = self._output
self._last_input = feedback_value
self._last_time = current_time
@property
def kp(self):
"""Aggressively the PID reacts to the current error with setting Proportional Gain"""
return self._kp
@kp.setter
def kp(self, value):
self._kp = value
@property
def ki(self):
"""Aggressively the PID reacts to the current error with setting Integral Gain"""
return self._ki
@ki.setter
def ki(self, value):
self._ki = value
@property
def kd(self):
"""Determines how aggressively the PID reacts to the current
error with setting Derivative Gain"""
return self._kd
@kd.setter
def kd(self, value):
self._kd = value
@property
def set_point(self):
"""The target point to the PID"""
return self._set_point
@set_point.setter
def set_point(self, value):
self._set_point = value
@property
def windup(self):
"""Integral windup, also known as integrator windup or reset windup,
refers to the situation in a PID feedback controller where
a large change in setpoint occurs (say a positive change)
and the integral terms accumulates a significant error
during the rise (windup), thus overshooting and continuing
to increase as this accumulated error is unwound
(offset by errors in the other direction).
The specific problem is the excess overshooting.
"""
return self._windup
@windup.setter
def windup(self, value):
self._windup = (-value, value)
@property
def sample_time(self):
"""PID that should be updated at a regular interval.
Based on a pre-determined sampe time, the PID decides if it should compute or
return immediately.
"""
return self._sample_time
@sample_time.setter
def sample_time(self, value):
self._sample_time = value
@property
def p(self):
return self._p_term
@property
def i(self):
return self._i_term
@property
def d(self):
return self._d_term
@property
def output(self):
"""PID result"""
return self._output
def log(self, message):
if not self._logger:
return
self._logger.warning(message)
def current_time(self):
try:
ret_time = time.monotonic()
except AttributeError:
ret_time = time.time()
return ret_time
def clamp_value(self, value, limits):
lower, upper = limits
if value is None:
return None
elif not lower and not upper:
return value
elif (upper is not None) and (value > upper):
return upper
elif (lower is not None) and (value < lower):
return lower
return value

View File

@@ -0,0 +1,839 @@
#
# Copyright (c) 2022, Diogo Silva "Soloam"
# Creative Commons BY-NC-SA 4.0 International Public License
# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/)
#
"""
PID Controller.
For more details about this sensor, please refer to the documentation at
https://github.com/soloam/ha-pid-controller/
"""
from __future__ import annotations
import logging
from math import floor, ceil
from typing import Any, Mapping, Optional
import voluptuous as vol
from _sha1 import sha1
from homeassistant.components.sensor import SensorEntity, SensorDeviceClass
from homeassistant.const import (
CONF_ENTITY_ID,
CONF_NAME,
CONF_ICON,
CONF_UNIQUE_ID,
EVENT_HOMEASSISTANT_START,
STATE_UNAVAILABLE,
CONF_MINIMUM,
CONF_MAXIMUM,
CONF_UNIT_OF_MEASUREMENT,
CONF_DEVICE_CLASS,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.template import result_as_boolean
# pylint: disable=wildcard-import, unused-wildcard-import
from .const import *
from .pidcontroller import PIDController as PID
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_ENABLED, default=DEFAULT_ENABLED): cv.template,
vol.Optional(CONF_ICON, default=DEFAULT_ICON): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Required(CONF_SETPOINT): cv.template,
vol.Optional(CONF_PROPORTIONAL, default=0): cv.template,
vol.Optional(CONF_INTEGRAL, default=0): cv.template,
vol.Optional(CONF_DERIVATIVE, default=0): cv.template,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_INVERT, default=False): cv.template,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.template,
vol.Optional(CONF_MINIMUM, default=DEFAULT_MINIMUM): cv.template,
vol.Optional(CONF_MAXIMUM, default=DEFAULT_MAXIMUM): cv.template,
vol.Optional(CONF_ROUND, default=DEFAULT_ROUND): cv.template,
vol.Optional(CONF_SAMPLE_TIME, default=DEFAULT_SAMPLE_TIME): cv.template,
vol.Optional(CONF_WINDUP, default=DEFAULT_WINDUP): cv.template,
vol.Optional(
CONF_UNIT_OF_MEASUREMENT, default=DEFAULT_UNIT_OF_MEASUREMENT
): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.template,
}
)
)
# pylint: disable=unused-argument
async def async_setup_platform(
hass: HomeAssistant, config, async_add_entities, discovery_info=None
):
enabled = config.get(CONF_ENABLED)
icon = config.get(CONF_ICON)
set_point = config.get(CONF_SETPOINT)
proportional = config.get(CONF_PROPORTIONAL)
integral = config.get(CONF_INTEGRAL)
derivative = config.get(CONF_DERIVATIVE)
invert = config.get(CONF_INVERT)
precision = config.get(CONF_PRECISION)
minimum = config.get(CONF_MINIMUM)
maximum = config.get(CONF_MAXIMUM)
round_type = config.get(CONF_ROUND)
sample_time = config.get(CONF_SAMPLE_TIME)
windup = config.get(CONF_WINDUP)
device_class = config.get(CONF_DEVICE_CLASS)
## Process Templates.
for template in [
enabled,
icon,
set_point,
sample_time,
windup,
proportional,
integral,
derivative,
invert,
precision,
minimum,
maximum,
round_type,
device_class,
]:
if template is not None:
template.hass = hass
## Set up platform.
async_add_entities(
[
PidController(
hass,
config.get(CONF_UNIQUE_ID),
config.get(CONF_NAME),
enabled,
icon,
set_point,
config.get(CONF_UNIT_OF_MEASUREMENT),
device_class,
sample_time,
windup,
proportional,
integral,
derivative,
invert,
minimum,
maximum,
round_type,
precision,
config.get(CONF_ENTITY_ID),
)
]
)
# pylint: disable=r0902
class PidController(SensorEntity):
# pylint: disable=r0913
def __init__(
self,
hass: HomeAssistant,
unique_id,
name,
enabled,
icon,
set_point,
unit_of_measurement,
device_class,
sample_time,
windup,
proportional,
integral,
derivative,
invert,
minimum,
maximum,
round_type,
precision,
entity_id,
):
self._attr_name = name
self._attr_native_unit_of_measurement = unit_of_measurement
self._enabled_template = enabled
self._icon_template = icon
self._set_point_template = set_point
self._device_class_template = device_class
self._sample_time_template = sample_time
self._windup_template = windup
self._sensor_state = 0
self._proportional_template = proportional
self._integral_template = integral
self._derivative_template = derivative
self._invert_template = invert
self._minimum_template = minimum
self._maximum_template = maximum
self._round_template = round_type
self._precision_template = precision
self._entities = []
self._force_update = []
self._reset_pid = []
self._feedback_pid = []
self._pid = None
self._source = entity_id
self._tunning = False
self._updating = False
self._tunnig_calculating = False
self._tunning_data = {}
self._enabled_entities = []
self._p_entities = []
self._i_entities = []
self._d_entities = []
self._get_entities()
self._attr_unique_id = (
str(
sha1(
";".join(
[
str(set_point),
str(proportional),
str(integral),
str(derivative),
]
).encode("utf-8")
).hexdigest()
)
if unique_id == "__legacy__"
else unique_id
)
@property
def native_value(self):
"""Return the state of the sensor."""
if not self.enabled:
return self.minimum
state = 0
try:
state = float(self._sensor_state) / 100
except ValueError:
state = 0
if self.minimum > self.maximum:
state = 0
units = self.units * state
state = self.minimum + units
precision = pow(10, self.precision)
if self.round == ROUND_FLOOR:
state = floor(state * precision) / precision
elif self.round == ROUND_CEIL:
state = ceil(state * precision) / precision
else:
state = round(state, self.precision)
if self.precision == 0:
state = int(state)
return state if self.available else STATE_UNAVAILABLE
@property
def available(self) -> bool:
"""Return True if entity is available."""
return True
@property
def enabled(self) -> bool:
"""Enabled"""
if self._enabled_template is not None:
try:
enabled = self._enabled_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_ENABLED)
return DEFAULT_ENABLED
return bool(result_as_boolean(enabled))
return DEFAULT_ENABLED
@property
def tunning(self) -> bool:
"""Returns Tunning"""
return self._tunning
@property
def icon(self) -> str | None:
"""Returns Icon"""
icon = DEFAULT_ICON
if self._icon_template is not None:
try:
icon = self._icon_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_ICON)
icon = DEFAULT_ICON
return icon
@property
def units(self) -> float:
return self.maximum - self.minimum
@property
def raw_state(self) -> float:
"""Return the state of the sensor."""
state = 0
try:
state = float(self._sensor_state) / 100
except ValueError:
state = 0
return float(state) if self.available else 0
@property
def extra_state_attributes(self) -> Optional[Mapping[str, Any]]:
"""Return entity specific state attributes."""
state_attr = {}
for attr in ATTR_TO_PROPERTY:
try:
if getattr(self, attr) is not None:
state_attr[attr] = getattr(self, attr)
except AttributeError:
continue
return state_attr
@property
def source(self) -> float:
"""Returns Response"""
source_state = self.hass.states.get(self._source)
if not source_state:
return float(0)
try:
state = float(source_state.state)
except ValueError:
state = float(0)
return float(state)
@property
def set_point(self) -> float:
"""Returns Set Point"""
if self._set_point_template is not None:
try:
set_point = self._set_point_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_SETPOINT)
return float(0)
try:
set_point = float(set_point)
except (ValueError):
set_point = 0
return float(set_point)
return float(0)
@property
def device_class(self) -> SensorDeviceClass:
"""Returns Device Class"""
if self._device_class_template is not None:
try:
device_class = self._device_class_template.async_render(
parse_result=False
)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_DEVICE_CLASS)
device_class = DEFAULT_DEVICE_CLASS
return device_class
@property
def sample_time(self) -> int:
"""Returns Sample Time"""
if self._sample_time_template is not None:
try:
sample_time = self._sample_time_template.async_render(
parse_result=False
)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_SAMPLE_TIME)
return int(DEFAULT_SAMPLE_TIME)
try:
sample_time = int(float(sample_time))
except ValueError:
sample_time = int(DEFAULT_SAMPLE_TIME)
return int(sample_time)
return int(DEFAULT_SAMPLE_TIME)
@property
def windup(self) -> int:
"""Returns Windup"""
if self._windup_template is not None:
try:
windup = self._windup_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_WINDUP)
return int(DEFAULT_WINDUP)
try:
windup = int(float(windup))
except ValueError:
windup = int(DEFAULT_WINDUP)
return int(windup)
return int(DEFAULT_WINDUP)
@property
def proportional(self) -> float:
"""Returns Proportional Band"""
if self._proportional_template is not None:
try:
proportional = self._proportional_template.async_render(
parse_result=False
)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_PROPORTIONAL)
return 0
try:
proportional = float(proportional)
except (ValueError):
proportional = 0
if self.invert:
proportional = proportional * -1
return float(proportional)
return float(0)
@property
def integral(self) -> float:
"""Returns Internal Band"""
if self._integral_template is not None:
try:
integral = self._integral_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_INTEGRAL)
return float(0)
try:
integral = float(integral)
except ValueError:
integral = 0
if self.invert:
integral = integral * -1
return float(integral)
return float(0)
@property
def derivative(self) -> float:
"""Returns Derivative Band"""
if self._derivative_template is not None:
try:
derivative = self._derivative_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_DERIVATIVE)
return float(0)
try:
derivative = float(derivative)
except ValueError:
derivative = 0
if self.invert:
derivative = derivative * -1
return float(derivative)
return float(0)
@property
def minimum(self) -> float:
"""Returns Minimum"""
if self._minimum_template is not None:
try:
minimum = self._minimum_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_MINIMUM)
return float(DEFAULT_MINIMUM)
try:
minimum = float(minimum)
except ValueError:
minimum = float(DEFAULT_MINIMUM)
return minimum
return DEFAULT_MINIMUM
@property
def maximum(self) -> float:
"""Returns Maximum"""
if self._maximum_template is not None:
try:
maximum = self._maximum_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_MAXIMUM)
return float(DEFAULT_MAXIMUM)
try:
maximum = float(maximum)
except ValueError:
maximum = float(DEFAULT_MAXIMUM)
return maximum
return DEFAULT_MAXIMUM
@property
def round(self) -> str:
"""Returns Round Type"""
if self._round_template is not None:
try:
round_type = self._round_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_ROUND)
return False
return str(round_type).lower()
return DEFAULT_ROUND
@property
def invert(self) -> bool:
"""Returns Inverted State"""
if self._invert_template is not None:
try:
invert = self._invert_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_INVERT)
return False
return bool(result_as_boolean(invert))
return False
@property
# pylint: disable=invalid-name
def p(self) -> float:
"""Calculated Proportional Gain"""
if not self._pid:
return float(0)
p = self._pid.p
return float(p)
@property
# pylint: disable=invalid-name
def i(self) -> float:
"""Calculated Integral Gain"""
if not self._pid:
return float(0)
i = self._pid.i
return float(i)
@property
# pylint: disable=invalid-name
def d(self) -> float:
"""Calculated Derivative Gain"""
if not self._pid:
return float(0)
d = self._pid.d
return float(d)
@property
def precision(self) -> int:
"""Returns Precision"""
if self._precision_template is not None:
try:
precision = self._precision_template.async_render(parse_result=False)
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_PRECISION)
return int(DEFAULT_PRECISION)
try:
precision = int(float(precision))
except ValueError:
precision = int(DEFAULT_PRECISION)
return int(precision)
return int(DEFAULT_PRECISION)
@staticmethod
def show_template_exception(ex, field) -> None:
"""Show Template Erros"""
if ex.args and ex.args[0].startswith("UndefinedError: 'None' has no attribute"):
# Common during HA startup - so just a warning
_LOGGER.warning(ex)
else:
_LOGGER.error('Error parsing template for field "%s": %s', field, ex)
def _get_entities(self) -> None:
self._entities = []
self._force_update = []
if self._icon_template is not None:
try:
info = self._icon_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_ICON)
else:
self._entities += info.entities
if self._set_point_template is not None:
try:
info = self._set_point_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_SETPOINT)
else:
self._entities += info.entities
self._reset_pid += info.entities
if self._device_class_template is not None:
try:
info = self._device_class_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_DEVICE_CLASS)
else:
self._entities += info.entities
self._force_update += info.entities
if self._sample_time_template is not None:
try:
info = self._sample_time_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_SAMPLE_TIME)
else:
self._entities += info.entities
if self._windup_template is not None:
try:
info = self._windup_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_WINDUP)
else:
self._entities += info.entities
if self._enabled_template is not None:
try:
info = self._enabled_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_ENABLED)
else:
self._entities += info.entities
self._reset_pid += info.entities
self._force_update += info.entities
self._enabled_entities += info.entities
if self._proportional_template is not None:
try:
info = self._proportional_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_PROPORTIONAL)
else:
self._entities += info.entities
self._p_entities += info.entities
if self._integral_template is not None:
try:
info = self._integral_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_INTEGRAL)
else:
self._entities += info.entities
self._i_entities += info.entities
if self._derivative_template is not None:
try:
info = self._derivative_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_DERIVATIVE)
else:
self._entities += info.entities
self._d_entities += info.entities
if self._precision_template is not None:
try:
info = self._precision_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_PRECISION)
else:
self._entities += info.entities
self._force_update += info.entities
if self._minimum_template is not None:
try:
info = self._minimum_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_MINIMUM)
else:
self._entities += info.entities
self._force_update += info.entities
if self._maximum_template is not None:
try:
info = self._maximum_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_MAXIMUM)
else:
self._entities += info.entities
self._force_update += info.entities
if self._round_template is not None:
try:
info = self._round_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_ROUND)
else:
self._entities += info.entities
self._force_update += info.entities
if self._invert_template is not None:
try:
info = self._invert_template.async_render_to_info()
except (TemplateError, TypeError) as ex:
self.show_template_exception(ex, CONF_INVERT)
else:
self._entities += info.entities
self._force_update += info.entities
self._reset_pid += info.entities
self._entities += [self._source]
def reset_pid(self):
if self._pid:
self._pid.reset_pid()
def update(self) -> None:
"""Update the sensor state if it needed."""
self._update_sensor()
def _update_sensor(self, entity=None) -> None:
if entity in self._reset_pid:
self.reset_pid()
if not self.enabled:
return
source = self.source
set_point = self.set_point
if self.proportional == 0 and self.integral == 0 and self.derivative == 0:
if entity != self._source:
return
self._sensor_state = 0 if self.invert else 100
if source >= set_point:
self._sensor_state = 100 if self.invert else 0
else:
p_base = self.proportional
i_base = self.integral
d_base = self.derivative
if self._pid is None:
self._pid = PID(p_base, i_base, d_base, logger=_LOGGER)
else:
if p_base != self._pid.kp:
self._pid.kp = p_base
if i_base != self._pid.ki:
self._pid.ki = i_base
if d_base != self._pid.kd:
self._pid.kd = d_base
if self.sample_time != self._pid.sample_time:
self._pid.sample_time = self.sample_time
if self.windup != self._pid.windup:
self._pid.windup = self.windup
if set_point != self._pid.set_point:
self.reset_pid()
self._pid.set_point = set_point
if entity == self._source:
self._pid.update(source)
output = float(self._pid.output)
output = max(min(output, 100), 0)
self._sensor_state = output
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
# pylint: disable=unused-argument
@callback
def sensor_state_listener(entity, old_state, new_state):
"""Handle device state changes."""
last_state = self.state
self._update_sensor(entity=entity)
if last_state != self.state or entity in self._force_update:
self.async_schedule_update_ha_state(True)
# pylint: disable=unused-argument
@callback
def sensor_startup(event):
"""Update template on startup."""
self._update_sensor()
self.async_schedule_update_ha_state(True)
## process listners
for entity in self._entities:
async_track_state_change(self.hass, entity, sensor_state_listener)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, sensor_startup)
def update_entity(self, entity_id, state):
entity = self.hass.states.get(entity_id)
if not entity:
return
self.hass.states.async_set(entity_id, state, entity.attributes)

View File

@@ -0,0 +1,19 @@
#
# Copyright (c) 2022, Diogo Silva "Soloam"
# Creative Commons BY-NC-SA 4.0 International Public License
# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/)
#
reset_pid:
description: Reset a PID Controller
fields:
entity_id:
description: Name(s) of entities to reset
example: 'sensor.temperture_controller'
autotune_pid:
description: Autotune a PID Controller
fields:
entity_id:
description: Name(s) of entities to tune
example: 'sensor.temperture_controller'