diff --git a/lib/lib_div/ProcessControl/PID.cpp b/lib/lib_div/ProcessControl/PID.cpp new file mode 100644 index 000000000..3e5298087 --- /dev/null +++ b/lib/lib_div/ProcessControl/PID.cpp @@ -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); +} diff --git a/lib/lib_div/ProcessControl/PID.h b/lib/lib_div/ProcessControl/PID.h new file mode 100644 index 000000000..6ffa9a648 --- /dev/null +++ b/lib/lib_div/ProcessControl/PID.h @@ -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 diff --git a/lib/lib_div/ProcessControl/Timeprop.cpp b/lib/lib_div/ProcessControl/Timeprop.cpp new file mode 100644 index 000000000..c4d5e9eb8 --- /dev/null +++ b/lib/lib_div/ProcessControl/Timeprop.cpp @@ -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; +} diff --git a/lib/lib_div/ProcessControl/Timeprop.h b/lib/lib_div/ProcessControl/Timeprop.h new file mode 100644 index 000000000..c6df45be0 --- /dev/null +++ b/lib/lib_div/ProcessControl/Timeprop.h @@ -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 diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 9d33e6312..579e65dbb 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -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 ------------------- /*********************************************************************************************\ diff --git a/tasmota/xdrv_48_timeprop.ino b/tasmota/xdrv_48_timeprop.ino new file mode 100644 index 000000000..6e8537dbb --- /dev/null +++ b/tasmota/xdrv_48_timeprop.ino @@ -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 . +*/ + +/** + * 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. +*/ + +/** + * 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