Add berry heatfan

This commit is contained in:
Theo Arends 2025-01-29 16:25:38 +01:00
parent 9613004bb4
commit 95e93b8f33

View File

@ -0,0 +1,300 @@
#-
heatfan.be - HeatFan support for Tasmota
SPDX-FileCopyrightText: 2025 Theo Arends
SPDX-License-Identifier: GPL-3.0-only
-#
#------------------------------------------------------------
Version v1.2.4
ESP32-C2 based HeatFan WiFi Controller using Tasmota and Berry local CH Fan control
https://heatfan.eu/product/wifi-controller/
Template {"NAME":"HeatFan","GPIO":[4864,229,228,4737,4738,224,0,225,32,448,226,0,0,0,0,0,0,0,227,0,0],"FLAG":0,"BASE":1}
Module 0 # Enable above template
SO15 1 # (Light) COLOR/DIMMER/CT/CHANNEL (1)
SO68 0 # (Light) Enable Color PWM (0)
GroupTopic2 heatfans # Allow MQTT group topic control using button "Rotate Group Modes"
Relay1 to Relay6 control Leds for Modes 1 to 6 (interlocked)
GPIO00 = ADC1 range (temperature input from comparator)
GPIO01 = Relay6 (blue)
GPIO02 = Relay5 (red)
GPIO03 = ADC2 temperature (ADC_CH_TEMP)
GPIO04 = ADC3 temperature (ADC_CH_TMCU)
GPIO05 = Relay1 (red)
GPIO06 = gpio.digital_write (GPIO_FANS_EN)
GPIO07 = Relay2 (red)
GPIO08 = Button1
GPIO09 = PWM_i1 (20kHz fan control)
GPIO10 = Relay3 (red)
GPIO18 = Relay4 (red)
------------------------------------------------------------#
import persist
import string
import gpio
import webserver
class heatfan_cls
#------------------------------------------------------------
User specific parameters
Send temperature and speed to Home Automation
------------------------------------------------------------#
static domoticz = {
# hostname, temperature, speed
"heatfan1":[3149, 3150], # Domoticz Idx for temperature (3149) and speed (custom sensor) (3150)
"heatfan2":[3151, 3152],
"heatfan3":[3153, 3154]
}
def SendToHa()
if self.domoticz.contains(self.hostname)
tasmota.cmd(f"_DzSend1 {self.domoticz[self.hostname][0]},{self.temp_ext}")
tasmota.cmd(f"_DzSend1 {self.domoticz[self.hostname][1]},{self.speed}")
else
tasmota.log(f"HFN: Domoticz on '{self.hostname}' not configured", 3)
end
end
#------------------------------------------------------------
Mode specific parameters
------------------------------------------------------------#
static modes = {
# start_temp, max_temp, start_speed, max_speed, led_pin
1: [25, 100, 1, 5, 5], # Speed starts at 1 from 25°C and caps at 5 from 100°C
2: [25, 100, 1, 7, 7], # Speed starts at 1 from 25°C and caps at 7 from 100°C
3: [25, 70, 1, 8, 10], # Speed starts at 1 from 25°C and caps at 8 from 70°C
4: [25, 55, 1, 9, 18], # Speed starts at 1 from 25°C and caps at 9 from 55°C
5: [25, 40, 1, 10, 2], # Speed starts at 1 from 25°C and caps at 10 from 40°C
6: [10, 100, 10, 10, 1] # Speed starts at 10 from 10°C and caps at 10 from 100°C
}
#-----------------------------------------------------------#
static power_states = [ 0, 1, 10 ] # Relay power Off, On, Duty 10
# bool
var teleperiod
# int
var mode, mode_persist, mode_states, speed, dimmer_change
# float
var temp_ext, last_temp_ext, dz_temp_ext
# string
var version, hostname
#-----------------------------------------------------------#
def persist_load()
self.mode = 0 # Default restart mode (Off)
if persist.has("hf_mode")
self.mode = persist.find("hf_mode")
end
self.mode_persist = self.mode
end
def persist_update()
if self.mode_persist != self.mode
persist.setmember("hf_mode", self.mode)
persist.save(true)
self.mode_persist = self.mode
end
end
#-----------------------------------------------------------#
def init()
self.version = "1.2.4"
self.persist_load()
self.teleperiod = 0
self.mode_states = 6 # Number of modes (needs var as static doesn't work)
self.speed = 0
self.temp_ext = 0
self.last_temp_ext = 0
self.dz_temp_ext = 0
self.dimmer_change = -1
self.hostname = tasmota.cmd('Hostname')['Hostname']
tasmota.log(f"HFN: HeatFan {self.version} started on {self.hostname}", 2)
tasmota.add_driver(self)
end
#-----------------------------------------------------------#
def set_pin(pin, state)
if 2 == state
gpio.set_pwm(pin, self.power_states[state]) # Enable PWM and set low duty
else
gpio.pin_mode(pin, gpio.OUTPUT) # Disable PWM and set to Output
gpio.digital_write(pin, self.power_states[state]) # Off / On
end
end
def set_power_handler(cmd, idx)
var ps = tasmota.get_power()
self.mode = 0
for i:1..self.mode_states # Power1 to Power6
self.set_pin(self.modes[i][4], 0) # Turn all leds Off
if ps[i -1] == true
self.mode = i
end
end
if self.mode
self.set_pin(self.modes[self.mode][4], 2) # Turn active mode led On
end
self.persist_update()
end
def any_key(cmd, idx)
# idx = device_save << 24 | key << 16 | state << 8 | device
var state = (idx >> 8) & 0xff
if state == 10 # SINGLE
# Rotate power from off,1,2,3,4,5,6,off...
if self.mode == self.mode_states
tasmota.set_power(self.mode_states -1, false) # power offset from 0
else
tasmota.set_power(self.mode, true) # power offset from 0
end
end
end
#------------------------------------------------------------
Update fan speed based on temperature
Called almost every second by GetNextSensor() or at Teleperiod time
------------------------------------------------------------#
def rule_range(value)
self.temp_ext = value
if self.mode > 0
var start_temp = self.modes[self.mode][0]
var max_temp = self.modes[self.mode][1]
var start_speed = self.modes[self.mode][2]
var max_speed = self.modes[self.mode][3]
var speed = max_speed
var speeds = max_speed - start_speed # speeds = 0 .. 9
if speeds > 0
var temp_range = max_temp - start_temp # temp_range = 0 .. 99
if temp_range > 0
var temp_step = temp_range / speeds
speed = int((self.temp_ext - start_temp + temp_step) / temp_step) # 0 .. 10
if speed < 0 speed = 0 end
if speed < self.speed
var speed_dn = int((self.temp_ext - start_temp + temp_step + 0.6) / temp_step) # 0 .. 10
if speed_dn > speed
speed = speed_dn
end
end
if speed > max_speed speed = max_speed end
end
end
self.speed = speed
else
self.speed = 0
end
var dimmer = 0
if self.speed > 0
dimmer = 30 + (self.speed * 6)
end
# tasmota.log(f"HFN: Range1 {self.temp_ext}, Mode {self.mode}, Speeds {speeds}, Step {temp_step}, Speed {self.speed}, Dimmer {dimmer}", 3)
if dimmer != self.dimmer_change
if dimmer > 0
tasmota.cmd('Dimmer ' .. dimmer) # Set fan speed
self.set_pin(6, 1) # GPIO_FANS_EN
else
self.set_pin(6, 0) # GPIO_FANS_EN
tasmota.set_power(6, false) # Set Power7 off (power offset from 0)
end
self.dimmer_change = dimmer
end
if self.teleperiod == 1 # Needs to be executed here as tasmota.cmd destroys Response
self.SendToHa()
self.teleperiod = 0
end
end
#------------------------------------------------------------
Add sensor value to teleperiod
Called almost every second by GetNextSensor() or at Teleperiod time
------------------------------------------------------------#
def json_append()
var msg = string.format(",\"HeatFan\":{\"Mode\":%i,\"Speed\":%i,\"Temperature\":%.2f}",
self.mode, self.speed, self.temp_ext)
tasmota.response_append(msg)
if tasmota.global.tele_period == 0
self.teleperiod = 1;
end
end
#------------------------------------------------------------
Display sensor value in the web UI and react to button
Called every WebRefresh time
------------------------------------------------------------#
def web_sensor()
var max_speed = 0
if self.mode > 0
max_speed = self.modes[self.mode][3]
end
var msg = string.format("{s}Radiator Temperature{m}%.2f °C{e}"..
"{s}Fan Speed{m}%i / %i{e}",
self.temp_ext,
self.speed, max_speed)
tasmota.web_send_decimal(msg)
if webserver.has_arg("m_group_rotate")
# Rotate group topic power from off,1,2,3,4,5,6,off...
if self.mode == self.mode_states
tasmota.cmd(f"Publish cmnd/heatfans/Power{self.mode_states} 0")
else
tasmota.cmd(f"Publish cmnd/heatfans/Power{self.mode +1} 1")
end
end
end
def web_add_main_button()
webserver.content_send(f"<p></p><button onclick='la(\"&m_group_rotate=1\");'>Rotate Group Modes</button><div style='text-align:right;font-size:11px;color:#aaa;'>HeatFan {self.version}</div>")
end
end
heatfan = heatfan_cls()
tasmota.cmd("Interlock 1,2,3,4,5,6") # Interlock modes 1 to 6
tasmota.cmd("Interlock 1") # Enable interlock
var mode = heatfan.mode # Save persitent mode
tasmota.set_power(0, true) # Set mode 1 to reset interlock on restart
# any_key() support cycling power from off,1,2,3,4,5,6,off...
tasmota.cmd("SO1 1") # (Button) Control button single press (1)
tasmota.cmd("SO13 1") # (Button) Support only single press (1)
tasmota.cmd("SO73 1") # (Button) Detach buttons from relays (1)
tasmota.cmd("SO146 1") # (ESP32) Show ESP32 internal temperature sensor
tasmota.cmd("SO161 1") # (GUI) Disable display of state text (1)
tasmota.cmd("PwmFrequency 20000") # Fan frequency to 20kHz
tasmota.cmd("LedTable 0") # Disable gamma correction
tasmota.cmd("webtime 11,19") # Enable GUI time HH:MM:SS
tasmota.cmd("websensor2 0") # Disable display of ADC information in GUI
tasmota.cmd("webrefresh 1000") # Update GUI every second
tasmota.cmd("Tempres 2") # ADC2/3 temperature resolution
tasmota.cmd("Freqres 2") # ADC1 Range resolution
tasmota.cmd("Teleperiod 60") # Update HA every minute
#tasmota.cmd("AdcGpio0 400,1710,60,20") # ADC1 range
tasmota.add_rule("Analog#Range1", / value -> heatfan.rule_range(value))
if 0 == mode
tasmota.set_power(0, false) # Set mode 0
else
tasmota.set_power(mode -1, true) # Set stored mode
end