mirror of
https://github.com/arendst/Tasmota.git
synced 2025-07-24 11:16:34 +00:00
Add berry heatfan
This commit is contained in:
parent
9613004bb4
commit
95e93b8f33
300
tasmota/berry/examples/heatfan_1_2_4.be
Normal file
300
tasmota/berry/examples/heatfan_1_2_4.be
Normal 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
|
Loading…
x
Reference in New Issue
Block a user