Merge pull request #10412 from marcvs/pid-branch-revived

Pid branch revived
This commit is contained in:
Theo Arends 2021-01-07 13:51:40 +01:00 committed by GitHub
commit a814ec52a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1109 additions and 0 deletions

View File

@ -0,0 +1,234 @@
/**
* Copyright 2018 Colin Law
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* See Timeprop.h for Usage
*
**/
#include "PID.h"
PID::PID() {
m_initialised = 0;
m_last_sample_time = 0;
m_last_pv_update_time = 0;
m_last_power = 0.0;
}
void PID::initialise( double setpoint, double prop_band, double t_integral, double t_derivative,
double integral_default, int max_interval, double smooth_factor, unsigned char mode_auto, double manual_op ) {
m_setpoint = setpoint;
m_prop_band = prop_band;
m_t_integral = t_integral;
m_t_derivative = t_derivative;
m_integral_default = integral_default;
m_max_interval = max_interval;
m_smooth_factor= smooth_factor;
m_mode_auto= mode_auto;
m_manual_op = manual_op;
m_initialised = 1;
}
/* called regularly to calculate and return new power value */
double PID::tick( unsigned long nowSecs ) {
double power;
double factor;
if (m_initialised && m_last_pv_update_time) {
// we have been initialised and have been given a pv value
// check whether too long has elapsed since pv was last updated
if (m_max_interval > 0 && nowSecs - m_last_pv_update_time > m_max_interval) {
// yes, too long has elapsed since last PV update so go to fallback power
power = m_manual_op;
} else {
// is this the first time through here?
if (m_last_sample_time) {
// not first time
unsigned long delta_t = nowSecs - m_last_sample_time; // seconds
if (delta_t <= 0 || delta_t > m_max_interval) {
// too long since last sample so leave integral as is and set deriv to zero
m_derivative = 0;
} else {
if (m_smooth_factor > 0) {
// A derivative smoothing factor has been supplied
// smoothing time constant is td/factor but with a min of delta_t to stop overflows
int ts = m_t_derivative/m_smooth_factor > delta_t ? m_t_derivative/m_smooth_factor : delta_t;
factor = 1.0/(ts/delta_t);
} else {
// no integral smoothing so factor is 1, this makes smoothed_value the previous pv
factor = 1.0;
}
double delta_v = (m_pv - m_smoothed_value) * factor;
m_smoothed_value = m_smoothed_value + delta_v;
m_derivative = m_t_derivative * delta_v/delta_t;
// lock the integral if abs(previous integral + error) > prop_band/2
// as this means that P + I is outside the linear region so power will be 0 or full
// also lock if control is disabled
double error = m_pv - m_setpoint;
double pbo2 = m_prop_band/2.0;
double epi = error + m_integral;
if (epi < 0.0) epi = -epi; // abs value of error + m_integral
if (epi < pbo2 && m_mode_auto) {
if (m_t_integral <= 0) {
// t_integral is zero (or silly), set integral to one end or the other
// or half way if exactly on sp
if (error > 0.0) {
m_integral = pbo2;
} else if (error < 0) {
m_integral = -pbo2;
} else {
m_integral = 0.0;
}
} else {
m_integral = m_integral + error * delta_t/m_t_integral;
}
}
// clamp to +- 0.5 prop band widths so that it cannot push the zero power point outside the pb
// do this here rather than when integral is updated to allow for the fact that the pb may change dynamically
if ( m_integral < -pbo2 ) {
m_integral = -pbo2;
} else if (m_integral > pbo2) {
m_integral = pbo2;
}
}
} else {
// first time through, initialise context data
m_smoothed_value = m_pv;
// setup the integral term so that the power out would be integral_default if pv=setpoint
m_integral = (0.5 - m_integral_default)*m_prop_band;
m_derivative = 0.0;
}
double proportional = m_pv - m_setpoint;
if (m_prop_band == 0) {
// prop band is zero so drop back to on/off control with zero hysteresis
if (proportional > 0.0) {
power = 0.0;
} else if (proportional < 0.0) {
power = 1.0;
} else {
// exactly on sp so leave power as it was last time round
power = m_last_power;
}
}
else {
power = -1.0/m_prop_band * (proportional + m_integral + m_derivative) + 0.5;
}
// set power to disabled value if the loop is not enabled
if (!m_mode_auto) {
power = m_manual_op;
}
m_last_sample_time = nowSecs;
}
} else {
// not yet initialised or no pv value yet so set power to disabled value
power = m_manual_op;
}
if (power < 0.0) {
power = 0.0;
} else if (power > 1.0) {
power = 1.0;
}
m_last_power = power;
return power;
}
// call to pass in new process value
void PID::setPv( double pv, unsigned long nowSecs ){
m_pv = pv;
m_last_pv_update_time = nowSecs;
}
// methods to modify configuration data
void PID::setSp( double setpoint ) {
m_setpoint = setpoint;
}
void PID::setPb( double prop_band ) {
m_prop_band = prop_band;
}
void PID::setTi( double t_integral ) {
m_t_integral = t_integral;
}
void PID::setTd( double t_derivative ) {
m_t_derivative = t_derivative;
}
void PID::setInitialInt( double integral_default ) {
m_integral_default = integral_default;
}
void PID::setDSmooth( double smooth_factor ) {
m_smooth_factor = smooth_factor;
}
void PID::setAuto( unsigned char mode_auto ) {
m_mode_auto = mode_auto;
}
void PID::setManualPower( double manual_op ) {
m_manual_op = manual_op;
}
void PID::setMaxInterval( int max_interval ) {
m_max_interval = max_interval;
}
double PID::getPv() {
return(m_pv);
}
double PID::getSp() {
return(m_setpoint);
}
double PID::getPb() {
return(m_prop_band);
}
double PID::getTi() {
return(m_t_integral);
}
double PID::getTd() {
return(m_t_derivative);
}
double PID::getInitialInt() {
return(m_integral_default);
}
double PID::getDSmooth() {
return(m_smooth_factor);
}
unsigned char PID::getAuto() {
return(m_mode_auto);
}
double PID::getManualPower() {
return(m_manual_op);
}
int PID::getMaxInterval() {
return(m_max_interval);
}

View File

@ -0,0 +1,101 @@
/**
* Copyright 2018 Colin Law
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* A PID control class
*
* Github repository https://github.com/colinl/process-control.git
*
* Given ...
*
* Usage:
* First call initialise(), see below for parameters then
* ...
* The functions require a parameter nowSecs which is a representation of the
* current time in seconds. The absolute value of this is immaterial, it is
* used for relative timing only.
*
**/
#ifndef PID_h
#define PID_h
class PID {
public:
PID();
/*
Initialiser given
current time in seconds
*/
void initialise( double setpoint, double prop_band, double t_integral, double t_derivative,
double integral_default, int max_interval, double smooth_factor, unsigned char mode_auto, double manual_op );
/* called regularly to calculate and return new power value */
double tick(unsigned long nowSecs);
// call to pass in new process value
void setPv( double pv, unsigned long nowSecs );
// methods to modify configuration data
void setSp( double setpoint );
void setPb( double prop_band );
void setTi( double t_integral );
void setTd( double t_derivative );
void setInitialInt( double integral_default );
void setDSmooth( double smooth_factor );
void setAuto( unsigned char mode_auto );
void setManualPower( double manual_op );
void setMaxInterval( int max_interval );
double getPv();
double getSp();
double getPb();
double getTi();
double getTd();
double getInitialInt();
double getDSmooth();
unsigned char getAuto();
double getManualPower();
int getMaxInterval();
private:
double m_pv;
double m_setpoint;
double m_prop_band;
double m_t_integral;
double m_t_derivative;
double m_integral_default;
double m_smooth_factor;
unsigned char m_mode_auto;
double m_manual_op;
int m_max_interval;
double m_last_power;
unsigned char m_initialised;
unsigned long m_last_pv_update_time; // the time of last pv update secs
unsigned long m_last_sample_time; // the time of the last tick() run
double m_smoothed_value;
double m_integral;
double m_derivative ;
};
#endif // Timeprop_h

View File

@ -0,0 +1,94 @@
/**
* Copyright 2018 Colin Law
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* See Timeprop.h for Usage
*
**/
#include "Timeprop.h"
void Timeprop::initialise( int cycleTime, int deadTime, unsigned char invert, float fallbackPower, int maxUpdateInterval,
unsigned long nowSecs) {
m_cycleTime = cycleTime;
m_deadTime = deadTime;
m_invert = invert;
m_fallbackPower = fallbackPower;
m_maxUpdateInterval = maxUpdateInterval;
m_dtoc = (float)deadTime/cycleTime;
m_opState = 0;
setPower(m_fallbackPower, nowSecs);
}
/* set current power required 0:1, given power and current time in seconds */
void Timeprop::setPower( float power, unsigned long nowSecs ) {
if (power < 0.0) {
power = 0.0;
} else if (power >= 1.0) {
power = 1.0;
}
m_power = power;
m_lastPowerUpdateTime = nowSecs;
};
/* called regularly to provide new output value */
/* returns new o/p state 0, 1 */
int Timeprop::tick( unsigned long nowSecs) {
int newState;
float wave;
float direction;
float effectivePower;
// check whether too long has elapsed since power was last updated
if (m_maxUpdateInterval > 0 && nowSecs - m_lastPowerUpdateTime > m_maxUpdateInterval) {
// yes, go to fallback power
setPower(m_fallbackPower, nowSecs);
}
wave = (nowSecs % m_cycleTime)/(float)m_cycleTime;
// determine direction of travel and convert to triangular wave
if (wave < 0.5) {
direction = 1; // on the way up
wave = wave*2;
} else {
direction = -1; // on the way down
wave = (1 - wave)*2;
}
// if a dead_time has been supplied for this o/p then adjust power accordingly
if (m_deadTime > 0 && m_power > 0.0 && m_power < 1.0) {
effectivePower = (1.0-2.0*m_dtoc)*m_power + m_dtoc;
} else {
effectivePower = m_power;
}
// cope with end cases in case values outside 0..1
if (effectivePower <= 0.0) {
newState = 0; // no heat
} else if (effectivePower >= 1.0) {
newState = 1; // full heat
} else {
// only allow power to come on on the way down and off on the way up, to reduce short pulses
if (effectivePower >= wave && direction == -1) {
newState = 1;
} else if (effectivePower <= wave && direction == 1) {
newState = 0;
} else {
// otherwise leave it as it is
newState = m_opState;
}
}
m_opState = newState;
return m_invert ? (1-m_opState) : m_opState;
}

View File

@ -0,0 +1,85 @@
/**
* Copyright 2018 Colin Law
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* A class to generate a time proportioned digital output from a linear input
*
* Github repository https://github.com/colinl/process-control.git
*
* Given a required power value in the range 0.0 to 1.0 this class generates
* a time proportioned 0/1 output (representing OFF/ON) which averages to the
* required power value. The cycle time is configurable. If, for example, this
* is set to 10 minutes and the power input is 0.2 then the output will be on
* for two minutes in every ten minutes.
*
* A value for actuator dead time may be provided. If you have a device that
* takes a significant time to open/close then set this to the average of the
* open and close times. The algorithim will then adjust the output timing
* accordingly to ensure that the output is not switched more rapidly than
* the actuator can cope with.
*
* A facility to invert the output is provided which can be useful when used in
* refrigeration processes and similar.
*
* Usage:
* First call initialise(), see below for parameters then call setPower() to
* specify the current power required.
* Then regularly call tick() to determine the output state required.
* setPower may be called as often as required to change the power required.
* The functions require a parameter nowSecs which is a representation of the
* current time in seconds. The absolute value of this is immaterial, it is
* used for relative timing only.
*
**/
#ifndef Timeprop_h
#define Timeprop_h
class Timeprop {
public:
/*
Initialiser given
cycleTime seconds
actuator deadTime seconds
whether to invert the output
fallback power value if updates are not received within time below
max number of seconds to allow between power updates before falling back to default power (0 to disable)
current time in seconds
*/
void initialise( int cycleTime, int deadTime, unsigned char invert, float fallbackPower, int maxUpdateInterval,
unsigned long nowSecs);
/* set current power required 0:1, given power and current time in seconds */
void setPower( float power, unsigned long nowSecs );
/* called regularly to provide new output value */
/* returns new o/p state 0, 1 */
int tick(unsigned long nowSecs);
private:
int m_cycleTime; // cycle time seconds, float to force float calcs
int m_deadTime; // actuator action time seconds
unsigned char m_invert; // whether to invert the output
float m_dtoc; // deadTime/m_cycleTime
int m_opState; // current output state (before invert)
float m_power; // required power 0:1
float m_fallbackPower; // falls back to this if updates not received with max allowed timezone
int m_maxUpdateInterval; // max time between updates
unsigned long m_lastPowerUpdateTime; // the time of last power update secs
};
#endif // Timeprop_h

View File

@ -811,6 +811,11 @@
// -- Prometheus exporter ---------------------------
//#define USE_PROMETHEUS // Add support for https://prometheus.io/ metrics exporting over HTTP /metrics endpoint
// -- PID and Timeprop ------------------------------
// #define use TIMEPROP // Add support for the timeprop feature (+0k8 code)
// For details on the configuration please see the header of tasmota/xdrv_48_timeprop.ino
// #define USE_PID // Add suport for the PID feature (+11k1 code)
// For details on the configuration please see the header of tasmota/xdrv_49_pid.ino
// -- End of general directives -------------------
/*********************************************************************************************\

View File

@ -0,0 +1,172 @@
/*
xdrv_48_timeprop.ino - Timeprop support for Sonoff-Tasmota
Copyright (C) 2018 Colin Law and Thomas Herrmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Code to drive one or more relays in a time proportioned manner give a
* required power value.
*
* Given required power values in the range 0.0 to 1.0 the relays will be
* driven on/off in such that the average power suppled will represent
* the required power.
* The cycle time is configurable. If, for example, the
* period is set to 10 minutes and the power input is 0.2 then the output will
* be on for two minutes in every ten minutes.
*
* A value for actuator dead time may be provided. If you have a device that
* takes a significant time to open/close then set this to the average of the
* open and close times. The algorithim will then adjust the output timing
* accordingly to ensure that the output is not switched more rapidly than
* the actuator can cope with.
*
* A facility to invert the output is provided which can be useful when used in
* refrigeration processes and similar.
*
* In the case where only one relay is being driven the power value is set by
* writing the value to the mqtt topic cmnd/timeprop_setpower_0. If more than
* one relay is being driven (as might be the case for a heat/cool application
* where one relay drives the heater and the other the cooler) then the power
* for the second relay is written to topic cmnd/timeprop_setpower_1 and so on.
*
* To cope with the problem of temporary wifi failure etc a
* TIMEPROP_MAX_UPDATE_INTERVALS value is available. This can be set to the max
* expected time between power updates and if this time is exceeded then the
* power will fallback to a given safe value until a new value is provided. Set
* the interval to 0 to disable this feature.
*
* Usage:
* Place this file in the sonoff folder.
* Clone the library https://github.com/colinl/process-control.git from Github
* into a subfolder of lib.
* In user_config.h or user_config_override.h for a single relay, include
* code as follows:
#define USE_TIMEPROP // include the timeprop feature (+1.2k)
// for single output
#define TIMEPROP_NUM_OUTPUTS 1 // how many outputs to control (with separate alogorithm for each)
#define TIMEPROP_CYCLETIMES 60 // cycle time seconds
#define TIMEPROP_DEADTIMES 0 // actuator action time seconds
#define TIMEPROP_OPINVERTS false // whether to invert the output
#define TIMEPROP_FALLBACK_POWERS 0 // falls back to this if too long betwen power updates
#define TIMEPROP_MAX_UPDATE_INTERVALS 120 // max no secs that are allowed between power updates (0 to disable)
#define TIMEPROP_RELAYS 1 // which relay to control 1:8
* or for two relays:
#define USE_TIMEPROP // include the timeprop feature (+1.2k)
// for single output
#define TIMEPROP_NUM_OUTPUTS 2 // how many outputs to control (with separate alogorithm for each)
#define TIMEPROP_CYCLETIMES 60, 10 // cycle time seconds
#define TIMEPROP_DEADTIMES 0, 0 // actuator action time seconds
#define TIMEPROP_OPINVERTS false, false // whether to invert the output
#define TIMEPROP_FALLBACK_POWERS 0, 0 // falls back to this if too long betwen power updates
#define TIMEPROP_MAX_UPDATE_INTERVALS 120, 120 // max no secs that are allowed between power updates (0 to disable)
#define TIMEPROP_RELAYS 1, 2 // which relay to control 1:8
* Publish values between 0 and 1 to the topic(s) described above
*
**/
#ifdef USE_TIMEPROP
# include "Timeprop.h"
static Timeprop timeprops[TIMEPROP_NUM_OUTPUTS];
static int relayNos[TIMEPROP_NUM_OUTPUTS] = {TIMEPROP_RELAYS};
static long currentRelayStates = 0; // current actual relay states. Bit 0 first relay
static long timeprop_current_time_secs = 0; // a counter that counts seconds since initialisation
/* call this from elsewhere if required to set the power value for one of the timeprop instances */
/* index specifies which one, 0 up */
void Timeprop_Set_Power( int index, float power )
{
if (index >= 0 && index < TIMEPROP_NUM_OUTPUTS)
{
timeprops[index].setPower( power, timeprop_current_time_secs);
}
}
void Timeprop_Init()
{
// AddLog_P(LOG_LEVEL_INFO, PSTR("TPR: Timeprop Init"));
int cycleTimes[TIMEPROP_NUM_OUTPUTS] = {TIMEPROP_CYCLETIMES};
int deadTimes[TIMEPROP_NUM_OUTPUTS] = {TIMEPROP_DEADTIMES};
int opInverts[TIMEPROP_NUM_OUTPUTS] = {TIMEPROP_OPINVERTS};
int fallbacks[TIMEPROP_NUM_OUTPUTS] = {TIMEPROP_FALLBACK_POWERS};
int maxIntervals[TIMEPROP_NUM_OUTPUTS] = {TIMEPROP_MAX_UPDATE_INTERVALS};
for (int i=0; i<TIMEPROP_NUM_OUTPUTS; i++) {
timeprops[i].initialise(cycleTimes[i], deadTimes[i], opInverts[i], fallbacks[i],
maxIntervals[i], timeprop_current_time_secs);
}
}
void Timeprop_Every_Second() {
timeprop_current_time_secs++; // increment time
for (int i=0; i<TIMEPROP_NUM_OUTPUTS; i++) {
int newState = timeprops[i].tick(timeprop_current_time_secs);
if (newState != bitRead(currentRelayStates, relayNos[i]-1)){
// remove the third parameter below if using tasmota prior to v6.0.0
ExecuteCommandPower(relayNos[i], newState,SRC_IGNORE);
}
}
}
// called by the system each time a relay state is changed
void Timeprop_Xdrv_Power() {
// for a single relay the state is in the lsb of index, I have think that for
// multiple outputs then succesive bits will hold the state but have not been
// able to test that
currentRelayStates = XdrvMailbox.index;
}
/* struct XDRVMAILBOX { */
/* uint16_t valid; */
/* uint16_t index; */
/* uint16_t data_len; */
/* int16_t payload; */
/* char *topic; */
/* char *data; */
/* } XdrvMailbox; */
// To get here post with topic cmnd/timeprop_setpower_n where n is index into timeprops 0:7
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
#define XDRV_48 48
bool Xdrv48(byte function)
{
bool result = false;
switch (function) {
case FUNC_INIT:
Timeprop_Init();
break;
case FUNC_EVERY_SECOND:
Timeprop_Every_Second();
break;
case FUNC_SET_POWER:
Timeprop_Xdrv_Power();
break;
}
return result;
}
#endif // USE_TIMEPROP

418
tasmota/xdrv_49_pid.ino Normal file
View File

@ -0,0 +1,418 @@
/*
xdrv_49_pid.ino - PID algorithm plugin for Sonoff-Tasmota
Copyright (C) 2018 Colin Law and Thomas Herrmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Code to
*
* Usage:
* Place this file in the sonoff folder.
* Clone the library https://github.com/colinl/process-control.git from Github
* into a subfolder of lib.
* If you want to use a time proportioned relay output with this then also get
* xdrv_49_timeprop.ino
* In user_config.h or user_config_override.h include code as follows:
#define USE_PID // include the pid feature (+4.3k)
#define PID_SETPOINT 19.5 // Setpoint value. This is the process value that the process is
// aiming for.
// May be adjusted via MQTT using cmnd PidSp
#define PID_PROPBAND 5 // Proportional band in process units (eg degrees). This controls
// the gain of the loop and is the range of process value over which
// the power output will go from 0 to full power. The units are that
// of the process and setpoint, so for example in a heating
// application it might be set to 1.5 degrees.
// May be adjusted via MQTT using cmnd PidPb
#define PID_INTEGRAL_TIME 1800 // Integral time seconds. This is a setting for the integral time,
// in seconds. It represents the time constant of the integration
// effect. The larger the value the slower the integral effect will be.
// Obviously the slower the process is the larger this should be. For
// example for a domestic room heated by convection radiators a setting
// of one hour might be appropriate (in seconds). To disable the
// integral effect set this to a large number.
// May be adjusted via MQTT using cmnd PidTi
#define PID_DERIVATIVE_TIME 15 // Derivative time seconds. This is a setting for the derivative time,
// in seconds. It represents the time constant of the derivative effect.
// The larger the value the greater will be the derivative effect.
// Typically this will be set to somewhat less than 25% of the integral
// setting, once the integral has been adjusted to the optimum value. To
// disable the derivative effect set this to 0. When initially tuning a
// loop it is often sensible to start with derivative zero and wind it in
// once other parameters have been setup.
// May be adjusted via MQTT using cmnd PidTd
#define PID_INITIAL_INT 0.5 // Initial integral value (0:1). This is an initial value which is used
// to preset the integrated error value when the flow is deployed in
// order to assist in homing in on the setpoint the first time. It should
// be set to an estimate of what the power requirement might be in order
// to maintain the process at the setpoint. For example for a domestic
// room heating application it might be set to 0.2 indicating that 20% of
// the available power might be required to maintain the setpoint. The
// value is of no consequence apart from device restart.
#define PID_MAX_INTERVAL 300 // This is the maximum time in seconds that is expected between samples.
// It is provided to cope with unusual situations such as a faulty sensor
// that might prevent the node from being supplied with a process value.
// If no new process value is received for this time then the power is set
// to the value defined for PID_MANUAL_POWER.
// May be adjusted via MQTT using cmnd PidMaxInterval
#define PID_DERIV_SMOOTH_FACTOR 3 // In situations where the process sensor has limited resolution (such as
// the DS18B20), the use of deriviative can be problematic as when the
// process is changing only slowly the steps in the value cause spikes in
// the derivative. To reduce the effect of these this parameter can be
// set to apply a filter to the derivative term. I have found that with
// the DS18B20 that a value of 3 here can be beneficial, providing
// effectively a low pass filter on the derivative at 1/3 of the derivative
// time. This feature may also be useful if the process value is particularly
// noisy. The smaller the value the greater the filtering effect but the
// more it will reduce the effectiveness of the derivative. A value of zero
// disables this feature.
// May be adjusted via MQTT using cmnd PidDSmooth
#define PID_AUTO 1 // Auto mode 1 or 0 (for manual). This can be used to enable or disable
// the control (1=enable, auto mode, 0=disabled, manual mode). When in
// manual mode the output is set the value definded for PID_MANUAL_POWER
// May be adjusted via MQTT using cmnd PidAuto
#define PID_MANUAL_POWER 0 // Power output when in manual mode or fallback mode if too long elapses
// between process values
// May be adjusted via MQTT using cmnd PidManualPower
#define PID_UPDATE_SECS 0 // How often to run the pid algorithm (integer secs) or 0 to run the algorithm
// each time a new pv value is received, for most applictions specify 0.
// Otherwise set this to a time
// that is short compared to the response of the process. For example,
// something like 15 seconds may well be appropriate for a domestic room
// heating application.
// May be adjusted via MQTT using cmnd PidUpdateSecs
#define PID_USE_TIMPROP 1 // To use an internal relay for a time proportioned output to drive the
// process, set this to indicate which timeprop output to use. For a device
// with just one relay then this will be 1.
// It is then also necessary to define USE_TIMEPROP and set the output up as
// explained in xdrv_49_timeprop.ino
// To disable this feature leave this undefined (undefined, not defined to nothing).
#define PID_USE_LOCAL_SENSOR // If defined then the local sensor will be used for pv. Leave undefined if
// this is not required. The rate that the sensor is read is defined by TELE_PERIOD
// If not using the sensor then you can supply process values via MQTT using
// cmnd PidPv
#define PID_SHUTTER 1 // if using the PID to control a 3-way valve, create Tasmota Shutter and define the
// number of the shutter here. Otherwise leave this commented out
#define PID_REPORT_MORE_SETTINGS // If defined, the SENSOR output will provide more extensive json
// output in the PID section
// #define PID_BACKWARD_COMPATIBLE // Preserve the backward compatible reporting of PID power via
// `%topic%/PID {"power":"0.000"}` This is now available in
// `%topic$/SENSOR {..., "PID":{"PidPower":0.00}}`
// Don't use unless you know that you need it
* Help with using the PID algorithm and with loop tuning can be found at
* http://blog.clanlaw.org.uk/2018/01/09/PID-tuning-with-node-red-contrib-pid.html
* This is directed towards using the algorithm in the node-red node node-red-contrib-pid but the algorithm here is based on
* the code there and the tuning techique described there should work just the same.
*
**/
#ifdef USE_PID
#include "PID.h"
/* This might need to go to i18n.h */
#define D_PRFX_PID "Pid"
#define D_CMND_PID_SETPV "Pv"
#define D_CMND_PID_SETSETPOINT "Sp"
#define D_CMND_PID_SETPROPBAND "Pb"
#define D_CMND_PID_SETINTEGRAL_TIME "Ti"
#define D_CMND_PID_SETDERIVATIVE_TIME "Td"
#define D_CMND_PID_SETINITIAL_INT "Initint"
#define D_CMND_PID_SETDERIV_SMOOTH_FACTOR "DSmooth"
#define D_CMND_PID_SETAUTO "Auto"
#define D_CMND_PID_SETMANUAL_POWER "ManualPower"
#define D_CMND_PID_SETMAX_INTERVAL "MaxInterval"
#define D_CMND_PID_SETUPDATE_SECS "UpdateSecs"
const char kPIDCommands[] PROGMEM = D_PRFX_PID "|" // Prefix
D_CMND_PID_SETPV "|"
D_CMND_PID_SETSETPOINT "|"
D_CMND_PID_SETPROPBAND "|"
D_CMND_PID_SETINTEGRAL_TIME "|"
D_CMND_PID_SETDERIVATIVE_TIME "|"
D_CMND_PID_SETINITIAL_INT "|"
D_CMND_PID_SETDERIV_SMOOTH_FACTOR "|"
D_CMND_PID_SETAUTO "|"
D_CMND_PID_SETMANUAL_POWER "|"
D_CMND_PID_SETMAX_INTERVAL "|"
D_CMND_PID_SETUPDATE_SECS;
;
void (* const PIDCommand[])(void) PROGMEM = {
&CmndSetPv,
&CmndSetSp,
&CmndSetPb,
&CmndSetTi,
&cmndsetTd,
&CmndSetInitialInt,
&CmndSetDSmooth,
&CmndSetAuto,
&CmndSetManualPower,
&CmndSetMaxInterval,
&CmndSetUpdateSecs
};
static PID pid;
static int update_secs = PID_UPDATE_SECS <= 0 ? 0 : PID_UPDATE_SECS; // how often (secs) the pid alogorithm is run
static int max_interval = PID_MAX_INTERVAL;
static unsigned long last_pv_update_secs = 0;
static bool run_pid_now = false; // tells PID_Every_Second to run the pid algorithm
static long pid_current_time_secs = 0; // a counter that counts seconds since initialisation
void PID_Init()
{
pid.initialise( PID_SETPOINT, PID_PROPBAND, PID_INTEGRAL_TIME, PID_DERIVATIVE_TIME, PID_INITIAL_INT,
PID_MAX_INTERVAL, PID_DERIV_SMOOTH_FACTOR, PID_AUTO, PID_MANUAL_POWER );
}
void PID_Every_Second() {
static int sec_counter = 0;
pid_current_time_secs++; // increment time
// run the pid algorithm if run_pid_now is true or if the right number of seconds has passed or if too long has
// elapsed since last pv update. If too long has elapsed the the algorithm will deal with that.
if (run_pid_now || pid_current_time_secs - last_pv_update_secs > max_interval || (update_secs != 0 && sec_counter++ % update_secs == 0)) {
run_pid();
run_pid_now = false;
}
}
void PID_Show_Sensor() {
// Called each time new sensor data available, data in mqtt data in same format
// as published in tele/SENSOR
// Update period is specified in TELE_PERIOD
if (!isnan(TasmotaGlobal.temperature_celsius)) {
const float temperature = TasmotaGlobal.temperature_celsius;
// pass the value to the pid alogorithm to use as current pv
last_pv_update_secs = pid_current_time_secs;
pid.setPv(temperature, last_pv_update_secs);
// also trigger running the pid algorithm if we have been told to run it each pv sample
if (update_secs == 0) {
// this runs it at the next second
run_pid_now = true;
}
} else {
AddLog_P(LOG_LEVEL_ERROR, PSTR("PID: No local temperature sensor found"));
}
}
/* struct XDRVMAILBOX { */
/* uint16_t valid; */
/* uint16_t index; */
/* uint16_t data_len; */
/* int16_t payload; */
/* char *topic; */
/* char *data; */
/* } XdrvMailbox; */
void CmndSetPv(void) {
last_pv_update_secs = pid_current_time_secs;
pid.setPv(atof(XdrvMailbox.data), last_pv_update_secs);
// also trigger running the pid algorithm if we have been told to run it each pv sample
if (update_secs == 0) {
// this runs it at the next second
run_pid_now = true;
}
}
void CmndSetSp(void) {
pid.setSp(atof(XdrvMailbox.data));
ResponseCmndNumber(atof(XdrvMailbox.data));
}
void CmndSetPb(void) {
pid.setPb(atof(XdrvMailbox.data));
ResponseCmndNumber(atof(XdrvMailbox.data));
}
void CmndSetTi(void) {
pid.setTi(atof(XdrvMailbox.data));
ResponseCmndNumber(atof(XdrvMailbox.data));
}
void cmndsetTd(void) {
pid.setTd(atof(XdrvMailbox.data));
ResponseCmndNumber(atof(XdrvMailbox.data));
}
void CmndSetInitialInt(void) {
pid.setInitialInt(atof(XdrvMailbox.data));
ResponseCmndNumber(atof(XdrvMailbox.data));
}
void CmndSetDSmooth(void) {
pid.setDSmooth(atof(XdrvMailbox.data));
ResponseCmndNumber(atof(XdrvMailbox.data));
}
void CmndSetAuto(void) {
pid.setAuto(atoi(XdrvMailbox.data));
ResponseCmndNumber(atoi(XdrvMailbox.data));
}
void CmndSetManualPower(void) {
pid.setManualPower(atof(XdrvMailbox.data));
ResponseCmndNumber(atof(XdrvMailbox.data));
}
void CmndSetMaxInterval(void) {
pid.setMaxInterval(atoi(XdrvMailbox.data));
ResponseCmndNumber(atoi(XdrvMailbox.data));
}
// case CMND_PID_SETUPDATE_SECS:
// update_secs = atoi(XdrvMailbox.data) ;
// if (update_secs < 0)
// update_secs = 0;
void CmndSetUpdateSecs(void) {
update_secs = (atoi(XdrvMailbox.data));
if (update_secs < 0)
update_secs = 0;
ResponseCmndNumber(update_secs);
}
void PIDShowValues(void) {
char str_buf[FLOATSZ];
char chr_buf;
int i_buf;
double d_buf;
ResponseAppend_P(PSTR(",\"PID\":{"));
// #define D_CMND_PID_SETPV "Pv"
d_buf = pid.getPv();
dtostrfd(d_buf, 2, str_buf);
ResponseAppend_P(PSTR("\"PidPv\":%s,"), str_buf);
// #define D_CMND_PID_SETSETPOINT "Sp"
d_buf = pid.getSp();
dtostrfd(d_buf, 2, str_buf);
ResponseAppend_P(PSTR("\"PidSp\":%s,"), str_buf);
#ifdef PID_REPORT_MORE_SETTINGS
// #define D_CMND_PID_SETPROPBAND "Pb"
d_buf = pid.getPb();
dtostrfd(d_buf, 2, str_buf);
ResponseAppend_P(PSTR("\"PidPb\":%s,"), str_buf);
// #define D_CMND_PID_SETINTEGRAL_TIME "Ti"
d_buf = pid.getTi();
dtostrfd(d_buf, 2, str_buf);
ResponseAppend_P(PSTR("\"PidTi\":%s,"), str_buf);
// #define D_CMND_PID_SETDERIVATIVE_TIME "Td"
d_buf = pid.getTd();
dtostrfd(d_buf, 2, str_buf);
ResponseAppend_P(PSTR("\"PidTd\":%s,"), str_buf);
// #define D_CMND_PID_SETINITIAL_INT "Initint"
d_buf = pid.getInitialInt();
dtostrfd(d_buf, 2, str_buf);
ResponseAppend_P(PSTR("\"PidInitialInt\":%s,"), str_buf);
// #define D_CMND_PID_SETDERIV_SMOOTH_FACTOR "DSmooth"
d_buf = pid.getDSmooth();
dtostrfd(d_buf, 2, str_buf);
ResponseAppend_P(PSTR("\"PidDSmooth\":%s,"), str_buf);
// #define D_CMND_PID_SETAUTO "Auto"
chr_buf = pid.getAuto();
ResponseAppend_P(PSTR("\"PidAuto\":%d,"), chr_buf);
// #define D_CMND_PID_SETMANUAL_POWER "ManualPower"
d_buf = pid.getManualPower();
dtostrfd(d_buf, 2, str_buf);
ResponseAppend_P(PSTR("\"PidManualPower\":%s,"), str_buf);
// #define D_CMND_PID_SETMAX_INTERVAL "MaxInterval"
i_buf = pid.getMaxInterval();
ResponseAppend_P(PSTR("\"PidMaxInterval\":%d,"), i_buf);
// #define D_CMND_PID_SETUPDATE_SECS "UpdateSecs"
ResponseAppend_P(PSTR("\"PidUpdateSecs\":%d,"), update_secs);
#endif // PID_REPORT_MORE_SETTINGS
// The actual power value
d_buf = pid.tick(pid_current_time_secs);
dtostrfd(d_buf, 2, str_buf);
ResponseAppend_P(PSTR("\"PidPower\":%s"), str_buf);
ResponseAppend_P(PSTR("}"));
}
static void run_pid()
{
double power = pid.tick(pid_current_time_secs);
#ifdef PID_BACKWARD_COMPATIBLE
// This part is left inside to regularly publish the PID Power via
// `%topic%/PID {"power":"0.000"}`
char str_buf[FLOATSZ];
dtostrfd(power, 3, str_buf);
snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"%s\":\"%s\"}"), "power", str_buf);
MqttPublishPrefixTopic_P(TELE, "PID", false);
#endif // PID_BACKWARD_COMPATIBLE
#if defined PID_SHUTTER
// send output as a position from 0-100 to defined shutter
int pos = power * 100;
ShutterSetPosition(PID_SHUTTER, pos);
#endif //PID_SHUTTER
#if defined PID_USE_TIMPROP
// send power to appropriate timeprop output
Timeprop_Set_Power( PID_USE_TIMPROP-1, power );
#endif // PID_USE_TIMPROP
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
#define XDRV_49 49
bool Xdrv49(byte function)
{
bool result = false;
switch (function) {
case FUNC_INIT:
PID_Init();
break;
case FUNC_EVERY_SECOND:
PID_Every_Second();
break;
case FUNC_SHOW_SENSOR:
// only use this if the pid loop is to use the local sensor for pv
#if defined PID_USE_LOCAL_SENSOR
PID_Show_Sensor();
#endif // PID_USE_LOCAL_SENSOR
break;
case FUNC_COMMAND:
result = DecodeCommand(kPIDCommands, PIDCommand);
break;
case FUNC_JSON_APPEND:
PIDShowValues();
break;
}
return result;
}
#endif // USE_PID