When I bought a new house in 2021, I struggled a lot on how to interface the existing VMC (or MEV – Mechanical Extract Ventilation) to Home Assistant to control and monitor them in a more granular and automated fashion. The house is equipped with five Helty Flow Compact VMC’s by Alpac (Flow40 HRV with wall recess: the Helty concealed ventilation system (heltyair.com)) with no wifi nor remote control.
The only way to control them is via their modbus RS485 RTU interface. The 7 pins onboard connector provides the modbus pins in position 5 (data+) and 4 (data-).
I then decided to build a custom circuit to read available sensors from the VMC and control it remotely via modbus. What is needed:
- NodeMCU ESP8266 ESP-12F - https://www.amazon.it/sspa/click?ie=UTF8&spc=MTo1ODM2MjQxNDQ5MzU3NDA4OjE2ODU5MDA1NDU6c3BfYXRmOjIwMDM0NDM4MjQwMzk4OjowOjo&url=%2FAZDelivery-NodeMCU-esp8266-esp-12e-gratuito%2Fdp%2FB06Y1ZPNMS%2Fref%3Dsr_1_1_sspa%3Fcrid%3D1N8IC4D6SRGN0%26keywords%3Dnodemcu%2Besp8266%26qid%3D1685900545%26sprefix%3Dnodemcu%2Caps%2C115%26sr%3D8-1-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9hdGY%26psc%3D1%26smid%3DA1X7QLRQH87QA3
- RS-485 TTL to RS485 Converter – ANGEEK MAX485 - Modulo convertitore da RS-485 TTL a RS485 per kit fai da te Arduino (5 pezzi) : Amazon.it: Informatica
- A single side breadboard
- Two 15 pins headers for the nodemcu
- Two 4 pins headers for the modbus converter
- A two poles terminal block
- Some wirings and soldering stuff
- To control and interface the device to HA you need a running MQTT server (mosquitto)
- For interconnection of the circuit to the MEV you need several meters of 2-wires cable. I used a shielded 4x0.22 mmq cable (usually used for alarm systems). You can run several hundreds of meters of this cable without any issues. You can buy it from Tecnomat.
I used 2x 15 pin headers for the NodeMCU and 2x 4 pin header for the modbus converter. I added a two poles terminal block to power the NodeMCU from a 5V power adapter. Circuit diagram is the following:
The as built circuit is the following:
And here is the final one with NodeMCU and modbus converter:
The firmware on the nodemcu must be compiled using Arduino IDE (I used v2.1.0 but any version is OK). You need to adjust the settings with your wifi and MQTT info, change the modbus slave ID if different from the default 2, adjust the name of the ESP device (mine is VMC_Letto in the below example) and eventually change the used MQTT topics.
The first upload of the firmware must be from a wired connection. After the initial deploy, it will support OTA updates by browsing to http:///update.
////////////////////////////////////////////////////////////////////////
// //
// ModBus gateway to Helty VMC via NodeMCU and WIFI/MQTT support //
// //
// MQTT telemetry: //
// vmcs/vmc_sala/state (speed status) //
// vmcs/vmc_sala/info (json format) //
// IntTemperature (internal temperature) //
// ExtTemperature (external temperature) //
// Alarm (Alarm flag) //
// //
// MQTT Settings telemetry: //
// vmcs/vmc_sala/teleperiod (MQTT update period) //
// //
// MQTT Commands: //
// vmcs/vmc_sala/cmnd/teleperiod (MQTT update period) //
// vmcs/vmc_sala/cmnd/speed (0 to 7, speed setpoint) //
// vmcs/vmc_sala/LWT (Last Will Testament) //
// //
// Version History: //
// 1.0 - Initial Tests //
// 2.0 - First working build //
// 3.0 - Moved info to json //
////////////////////////////////////////////////////////////////////////
// include libraries
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <stdio.h>
#include <stdlib.h>
#include <ArduinoJson.h>
// required for OTA updates
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
// required for modbus communication
#include <ModbusRTU.h>
#include <SoftwareSerial.h>
//
// CONFIGURATION SECTION
// wi-fi and mqtt connection parameters
//
const char* ssid = "<YOUR WIFI SSID>"; // Enter your WiFi name
const char* password = "<YOUR WIFI PWD>"; // Enter WiFi password
const char* mqttServer = "<MQTT IP>"; // MQTT host IP
const int mqttPort = <MQTT PORT>; // MQTT TCP port
const char* mqttUser = "<MQTT USER>"; // MQTT username
const char* mqttPassword = "<MQTT PWD>"; // MQTT password
//
// END OF CONFIGURATION SECTION
//
// RS485 setup with NodeMCU
#define RE_DE D2 // Connect RE&DE terminal to pin GPIO4
#define RX D6 // pin GPIO12
#define TX D7 // pin GPIO13
#define SLAVE_ID 2 // slave ID
#define REG_COUNT 1 // registries count (does not support multiple readings)
#define SPEED_HREG 1000 // holding register 1000 - speed
#define INTTEMP_IREG 1000 // input register 1000 - internal temperature
#define EXTTEMP_IREG 1001 // input register 1001 - external temperature
#define ALARM_IREG 1006 // input register 1006 - alarm flags
// speeds settings
#define SPEED_ZERO 0x0000 // speed zero
#define SPEED_ONE 0x0001 // speed one
#define SPEED_TWO 0x0002 // speed two
#define SPEED_THREE 0x0003 // speed three
#define SPEED_FOUR 0x0004 // speed four
#define SPEED_HYPER 0x0005 // hyper speed
#define SPEED_NIGHT 0x0006 // night speed
#define SPEED_COOL 0x0007 // free cooling speed
// variables
uint16_t res;
uint16_t value;
String Msg = ""; // message for debug to MQTT
long period = 2000; // period between two consecutive mqtt updates (msec)
unsigned long time_now = 0; // timestamp for mqtt refresh
int speed = 0; // speed set
int speedO = 0; // speed set old value
char buffer [10];
char output [256];
// MQTT topics
const char* TOPIC_LWT = "vmcs/vmc_letto/LWT"; // last will testament
const char* TOPIC_CMD = "vmcs/vmc_letto/cmnd/#"; // all commands
const char* TOPIC_CMD1 = "vmcs/vmc_letto/cmnd/teleperiod"; // setup TelePeriod command
const char* TOPIC_CMD2 = "vmcs/vmc_letto/cmnd/speed"; // set speed command
// answers to command
const char* TOPIC_TELE = "vmcs/vmc_letto/state"; // speed status
const char* TOPIC_TELE1 = "vmcs/vmc_letto/teleperiod"; // MQTT update period
const char* TOPIC_TELE2 = "vmcs/vmc_letto/info"; // info
// initialize libraries for modbus
SoftwareSerial S(RX, TX);
ModbusRTU mb;
// initialize web server for OTA
AsyncWebServer server(80);
// initialize wi-fi client
WiFiClient espClient;
// initialize mqtt client
PubSubClient client(espClient);
// prepare for json encoding
StaticJsonDocument<200> doc;
//
// callback function to monitor modbus communication errors
//
bool cb(Modbus::ResultCode event, uint16_t transactionId, void* data) {
if (event != Modbus::EX_SUCCESS) {
Serial.printf_P("Request result: 0x%02X, Mem: %d\n", event, ESP.getFreeHeap());
}
return true;
}
//
// read holding register
// returns the register value
//
uint16_t rdHreg(uint16_t ADDRESS) {
if (!mb.slave()) { // Check if no transaction in progress
digitalWrite(RE_DE, HIGH);
mb.readHreg(SLAVE_ID, ADDRESS, &res, 1, cb); // Send Read Hreg from Modbus Server
delayMicroseconds(120);
digitalWrite(RE_DE,LOW);
while(mb.slave()) { // Check if transaction is active
mb.task();
yield();
}
Serial.println(res);
}
return res;
}
//
// write holding register
//
void wtHreg(uint16_t ADDRESS, uint16_t VALUE) {
if (!mb.slave()) { // Check if no transaction in progress
digitalWrite(RE_DE, HIGH);
mb.writeHreg(SLAVE_ID, ADDRESS, VALUE, cb); // Send write Hreg to Modbus Server
delayMicroseconds(120);
digitalWrite(RE_DE,LOW);
while(mb.slave()) { // Check if transaction is active
mb.task();
yield();
}
}
}
//
// read input register
// returns the register value
//
uint16_t rdIreg(uint16_t ADDRESS) {
if (!mb.slave()) { // Check if no transaction in progress
digitalWrite(RE_DE, HIGH);
mb.readIreg(SLAVE_ID, ADDRESS, &res, 1, cb); // Send Read Ireg from Modbus Server
delayMicroseconds(120);
digitalWrite(RE_DE,LOW);
while(mb.slave()) { // Check if transaction is active
mb.task();
yield();
}
Serial.println(res);
}
return res;
}
//
// MQTT connection function
//
void mqtt_connect() {
// Connect to MQTT broker
if (client.connect("VMC_Letto", mqttUser, mqttPassword,TOPIC_LWT,1,true,"Offline" )) {
// Connection to MQTT successful
Serial.println("Connected!");
// Subscribe to settings topics
client.subscribe(TOPIC_CMD); // subscribe to all command topics
client.publish(TOPIC_TELE1, itoa((int)period,buffer,10), true); // teleperiod
client.publish(TOPIC_LWT, "Online"); // last will testament
}
}
//
// WIFI connection function
//
void wifi_connect() {
// Connect to WIFI
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Connecting to WiFi..");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
}
}
//
// Callback function for MQTT subscriptions
//
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
String message = (char*)payload;
String thetopic = (char*)topic;
message = message.substring(0,length);
char buffer [5];
// prepare MQTT message for debug
Msg = "Message arrived in topic: " + thetopic + " -> " + message;
Serial.println(Msg);
// decode subscribed messages and return confirmation
// telegram period
if (thetopic == TOPIC_CMD1) {
Serial.print("TelePeriod=");
period = message.toInt();
Serial.println(message);
client.publish(TOPIC_TELE1, itoa(period,buffer,10), true);
}
// speed
if (thetopic == TOPIC_CMD2) {
Serial.print("Speed=");
speed = message.toInt();
Serial.println(message);
}
Serial.println();
Serial.println("-----------------------");
}
//
// setup routine
//
void setup() {
pinMode(RE_DE,OUTPUT); // direction pin
Serial.begin(115200); // start serial port
S.begin(19200, SWSERIAL_8N1); // setup software serial
mb.begin(&S); // start software serial
mb.master(); // start Master modbus processing
// Connnect to local wifi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
// Wait for wifi connection
Serial.println("Connecting to WiFi..");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
// activate http server for OTA updates
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Hi! I am VMC Letto.");
});
// Start ElegantOTA
AsyncElegantOTA.begin(&server);
server.begin();
Serial.println("HTTP server started");
// Set MQTT broker
client.setServer(mqttServer, mqttPort);
// Set the callback function for MQTT subscriptions
client.setCallback(mqtt_callback);
// Connect to MQTT broker
Serial.println("Connecting to MQTT...");
while (!client.connected()) {
Serial.print(".");
// Attempt connection to MQTT
if (client.connect("VMC_Letto", mqttUser, mqttPassword,TOPIC_LWT,1,true,"Offline" )) {
// Connection to MQTT successful
Serial.println("Connected!");
// Subscribe to settings topics
client.subscribe(TOPIC_CMD); // subscribe to all command topics
client.publish(TOPIC_TELE1, itoa((int)period,buffer,10), true); // teleperiod
client.publish(TOPIC_LWT, "Online"); // last will testament
}
}
value = rdHreg(SPEED_HREG);
client.publish(TOPIC_CMD2, itoa(value,buffer,10)); // update speed command
}
//
// main loop
//
void loop() {
// manage millis reset
if ((millis() - time_now) < 0) {
time_now = millis();
}
//Update MQTT every period (msec)
if ((millis() - time_now) >= period){
// check MQTT connection and reconnect
if (!client.connected()) {
mqtt_connect();
}
value = rdHreg(SPEED_HREG);
client.publish(TOPIC_TELE, itoa(value,buffer,10)); // update speed topic
// Add values in the document
//
value = rdIreg(INTTEMP_IREG); // internal temp x 0.1C
doc["IntTemperature"] = (float)value*0.1;
value = rdIreg(EXTTEMP_IREG); // external temp x 0.1C
doc["ExtTemperature"] = (float)value*0.1;
value = rdIreg(ALARM_IREG); // alarms
doc["Alarm"] = value;
serializeJson(doc, output);
client.publish(TOPIC_TELE2, output); // update alarm topic
// set next update
time_now += period;
}
// set speed of vmc
if (speed != speedO) {
switch (speed) {
case 0:
wtHreg(SPEED_HREG, SPEED_ZERO);
break;
case 1:
wtHreg(SPEED_HREG, SPEED_ONE);
break;
case 2:
wtHreg(SPEED_HREG, SPEED_TWO);
break;
case 3:
wtHreg(SPEED_HREG, SPEED_THREE);
break;
case 4:
wtHreg(SPEED_HREG, SPEED_FOUR);
break;
case 5:
wtHreg(SPEED_HREG, SPEED_HYPER);
break;
case 6:
wtHreg(SPEED_HREG, SPEED_NIGHT);
break;
case 7:
wtHreg(SPEED_HREG, SPEED_COOL);
break;
}
client.publish(TOPIC_TELE, itoa(speed,buffer,10), true);
speedO = speed;
}
// MQTT client loop
client.loop();
}
The as-built implementation for the five MEV is the following: it’s all enclosed into a wall cabinet with DIN rails. To mount the circuit into the rails I used a DIN Rail SCad project I found on the internet. You can adapt it to you liking and to your breadboard measures:
HA Configuration:
On HA I use two different dashboards: Mattias’ spectacular dashboard ( A different take on designing a Lovelace UI - Share your Projects! - Home Assistant Community (home-assistant.io)) for the tablets and Minimalist ( Lovelace UI • Minimalist - Share your Projects! - Home Assistant Community (home-assistant.io)) on the cell phones. As such I had to build two button card templates.
Tablet’s dashboard
The main Template:
base_vmc:
template:
- settings
- tilt
- extra_styles
variables:
state_on: >
[[[ return ['on'].indexOf(!entity || entity.state) !== -1; ]]]
state: >
[[[ return !entity || entity.state; ]]]
speed: >
[[[ return !entity || entity.attributes.preset_mode; ]]]
entity_id: >
[[[ return !entity || entity.entity_id; ]]]
entity_picture: >
[[[ return !entity || entity.attributes.entity_picture; ]]]
timeout: >
[[[ return !entity || Date.now() - Date.parse(entity.last_changed); ]]]
aspect_ratio: 1/1
show_state: false
show_label: true
show_icon: false
label: >
[[[
if (entity) {
return entity.attributes.ExtTemperature + "°C => " + entity.attributes.IntTemperature + "°C";
}
]]]
state_display: >
[[[ if (variables.state === true) return variables.translate_unknown; ]]]
tap_action:
ui_sound_tablet: |
[[[
let screensaver = states[variables.entity_tablet] === undefined ||
states[variables.entity_tablet].state;
if (variables.state === 'off' && screensaver === 'off') {
hass.callService('media_player', 'play_media', {
entity_id: variables.entity_browser_mod,
media_content_id: '/local/sound/on.m4a',
media_content_type: 'music'
});
}
if (variables.state_on && screensaver === 'off') {
hass.callService('media_player', 'play_media', {
entity_id: variables.entity_browser_mod,
media_content_id: '/local/sound/off.m4a',
media_content_type: 'music'
});
}
]]]
action: call-service
service: fan.decrease_speed
service_data:
entity_id: entity
percentage_step: 14
double_tap_action:
action: call-service
service: fan.increase_speed
service_data:
entity_id: entity
hold_action:
action: more-info
styles:
grid:
- grid-template-areas: |
"icon filter"
"n n"
"l l"
- grid-template-columns: repeat(2, 1fr)
- grid-template-rows: auto repeat(2, min-content)
- gap: 1.3%
- align-items: start
name:
- justify-self: start
- line-height: 121%
state:
- justify-self: start
- line-height: 115%
label:
- justify-self: start
- line-height: 115%
- font-size: 13px
card:
- border-radius: var(--button-card-border-radius)
- border-width: 0
- -webkit-tap-highlight-color: rgba(0,0,0,0)
- transition: none
- --mdc-ripple-color: >
[[[
return variables.state_on
? 'rgb(0, 0, 0)'
: '#97989c';
]]]
- color: >
[[[
return variables.state_on
? '#4b5254'
: '#97989c';
]]]
- background-color: >
[[[
return variables.state_on
? 'rgba(255, 255, 255, 0.85)'
: 'rgba(115, 115, 115, 0.25)';
]]]
custom_fields:
filter:
- align-self: start
- justify-self: end
custom_fields:
filter: >
[[[
return ((parseInt(entity.attributes.Alarm) & 1024) === 1024) ?
`<ha-icon icon="mdi:filter-remove-outline" style="width: 22px; height: 22px; color: red; animation: blink 2s ease infinite"></ha-icon>`
: `<ha-icon icon="mdi:filter-check-outline" style="width: 22px; height: 22px; color: gray;"></ha-icon>`
]]]
And the icon template:
icon_vmc:
styles:
custom_fields:
icon:
- width: 77%
- margin-left: -14%
- margin-top: 1%
custom_fields:
icon: >
[[[
if (variables.speed === 'stop') {
return '<font color=gray><ha-icon icon="mdi:fan-off" width="100%"></font></ha-icon>';
}
if (variables.speed === 'low') {
return '<font color=#116a8f><ha-icon icon="mdi:fan-speed-1" width="100%"></font></ha-icon>';
}
if (variables.speed === 'medium') {
return '<font color=#116a8f><ha-icon icon="mdi:fan-speed-2" width="100%"></font></ha-icon>';
}
if (variables.speed === 'high') {
return '<font color=#116a8f><ha-icon icon="mdi:fan-speed-3" width="100%"></font></ha-icon>';
}
if (variables.speed === 'super') {
return '<font color=#116a8f><ha-icon icon="mdi:fan-plus" width="100%"></font></ha-icon>';
}
if (variables.speed === 'hyper') {
return '<font color=#116a8f><ha-icon icon="mdi:weather-windy" width="100%"></font></ha-icon>';
}
if (variables.speed === 'night') {
return '<font color=#116a8f><ha-icon icon="mdi:sleep" width="100%"></font></ha-icon>';
}
if (variables.speed === 'cool') {
return '<font color=#116a8f><ha-icon icon="mdi:sun-snowflake" width="100%"></font></ha-icon>';
}
]]]
Ui-lovelace.yaml:
- type: custom:button-card
entity: fan.vmc_sala
name: Sala
template:
- base_vmc
- icon_vmc
Tap action decreases the speed, double tap action increases it.
External and Internal temperatures are reported as the button card label.
Status of filter is shown on the top right corner, it blinks red when it’s time to replace/clean the filter.
Icon changes based on the current preset mode.
Minimalist dashboard
Custom_card_templates.yaml:
###################
# vmc card #
###################
vmc_card:
show_name: false
show_icon: false
template:
- "icon_info_bg"
- "ulm_translation_engine"
hold_action:
action: "more-info"
styles:
grid:
- grid-template-areas: "'item1' 'item2' 'item3'"
- grid-template-columns: "1fr"
- grid-template-rows: "min-content min-content min-content"
- row-gap: "12px"
card:
- border-radius: "var(--border-radius)"
- box-shadow: "var(--box-shadow)"
- padding: "12px"
custom_fields:
item1:
card:
type: "custom:button-card"
template:
- "icon_info"
- "ulm_translation_engine"
tap_action:
action: "more-info"
entity: "[[[ return entity.entity_id ]]]"
name: "[[[ return entity.name ]]]"
label: >-
[[[
return (entity.attributes.ExtTemperature) + '°' + ' -> ' + entity.attributes.IntTemperature + '°';
]]]
icon: >
[[[
var icon = "mdi:fan-off";
var state = entity.attributes.preset_mode;
if (state =='low') {
return "mdi:fan-speed-1";
} else if (state =='medium') {
return "mdi:fan-speed-2";
} else if (state =='high') {
return "mdi:fan-speed-3";
} else if (state =='super') {
return "mdi:fan-plus";
} else if (state =='hyper') {
return "mdi:weather-windy";
} else if (state =='night') {
return "mdi:sleep";
} else if (state =='cool') {
return "mdi:sun-snowflake";
}
return icon;
]]]
state:
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'low' || entity.attributes.preset_mode == 'medium' || entity.attributes.preset_mode == 'high' || entity.attributes.preset_mode == 'super']]]"
styles:
icon:
- color: "rgba(var(--color-yellow),1)"
img_cell:
- background-color: "rgba(var(--color-yellow),0.2)"
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'night']]]"
styles:
icon:
- color: "rgba(var(--color, 255, 165, 0),1)"
img_cell:
- background-color: "rgba(var(--color, 255, 165, 0),0.2)"
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'hyper']]]"
styles:
icon:
- color: "rgba(var(--color-blue),1)"
img_cell:
- background-color: "rgba(var(--color-blue),0.2)"
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'cool']]]"
styles:
icon:
- color: "rgba(var(--color-red),1)"
img_cell:
- background-color: "rgba(var(--color-red),0.2)"
item2:
card:
type: "custom:button-card"
template: "list_4_items"
custom_fields:
item1:
card:
type: "custom:button-card"
template: "widget_icon"
icon: "mdi:power"
tap_action:
action: "call-service"
service: "fan.set_preset_mode"
service_data:
entity_id: "[[[ return entity.entity_id ]]]"
preset_mode: stop
item2:
card:
type: "custom:button-card"
template: "widget_icon"
icon: "mdi:sleep"
tap_action:
action: "call-service"
service: "fan.set_preset_mode"
service_data:
entity_id: "[[[ return entity.entity_id ]]]"
preset_mode: night
state:
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'night']]]"
styles:
icon:
- color: "rgba(var(--color, 255, 165, 0),1)"
img_cell:
- background-color: "rgba(var(--color, 255, 165, 0),0.2)"
item3:
card:
type: "custom:button-card"
template: "widget_icon"
icon: "mdi:weather-windy"
tap_action:
action: "call-service"
service: "fan.set_preset_mode"
service_data:
entity_id: "[[[ return entity.entity_id ]]]"
preset_mode: hyper
state:
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'hyper']]]"
styles:
icon:
- color: "rgba(var(--color-blue),1)"
img_cell:
- background-color: "rgba(var(--color-blue),0.2)"
item4:
card:
type: "custom:button-card"
template: "widget_icon"
icon: "mdi:sun-snowflake"
tap_action:
action: "call-service"
service: "fan.set_preset_mode"
service_data:
entity_id: "[[[ return entity.entity_id ]]]"
preset_mode: cool
state:
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'cool']]]"
styles:
icon:
- color: "rgba(var(--color-red),1)"
img_cell:
- background-color: "rgba(var(--color-red),0.2)"
item3:
card:
type: "custom:button-card"
template: "list_4_items"
custom_fields:
item1:
card:
type: "custom:button-card"
template: "widget_icon"
icon: "mdi:numeric-1"
tap_action:
action: "call-service"
service: "fan.set_preset_mode"
service_data:
entity_id: "[[[ return entity.entity_id ]]]"
preset_mode: low
state:
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'low']]]"
styles:
icon:
- color: "rgba(var(--color-yellow),1)"
img_cell:
- background-color: "rgba(var(--color-yellow),0.2)"
item2:
card:
type: "custom:button-card"
template: "widget_icon"
icon: "mdi:numeric-2"
tap_action:
action: "call-service"
service: "fan.set_preset_mode"
service_data:
entity_id: "[[[ return entity.entity_id ]]]"
preset_mode: medium
state:
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'medium']]]"
styles:
icon:
- color: "rgba(var(--color-yellow),1)"
img_cell:
- background-color: "rgba(var(--color-yellow),0.2)"
item3:
card:
type: "custom:button-card"
template: "widget_icon"
icon: "mdi:numeric-3"
tap_action:
action: "call-service"
service: "fan.set_preset_mode"
service_data:
entity_id: "[[[ return entity.entity_id ]]]"
preset_mode: high
state:
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'high']]]"
styles:
icon:
- color: "rgba(var(--color-yellow),1)"
img_cell:
- background-color: "rgba(var(--color-yellow),0.2)"
item4:
card:
type: "custom:button-card"
template: "widget_icon"
icon: "mdi:numeric-4"
tap_action:
action: "call-service"
service: "fan.set_preset_mode"
service_data:
entity_id: "[[[ return entity.entity_id ]]]"
preset_mode: super
state:
- operator: "template"
value: "[[[return entity.attributes.preset_mode == 'super']]]"
styles:
icon:
- color: "rgba(var(--color-yellow),1)"
img_cell:
- background-color: "rgba(var(--color-yellow),0.2)"
Lovelace-minimalist.yaml:
- type: 'custom:button-card'
template: vmc_card
name: Sala
entity: fan.vmc_sala
Automation will be implemented using Node-Red …. Stay tuned, coming soon!