Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #include <Arduino.h>
- #include <avr/wdt.h>
- #include <stdarg.h>
- #include <stdio.h>
- #include "cc1101.h"
- #define ONBOARD_LED 7
- #define FAN_INTERVAL 5000 // RF link interval
- #define LED_FLASH_TIME 25 // ms
- #define TX_RETRY_COUNT 5
- #define COMFORT_TEMP_POLL_INTERVAL_MS 300000UL // 5 minutes between successful polls
- #define COMFORT_TEMP_RETRY_INTERVAL_MS 30000UL // 30 s between failed retries — spaces out blocking transmitData calls so pollFanState etc. get turns in between
- enum ModuleState
- {
- JUST_BOOTED,
- RF15_PAIRINGSMODE,
- PAIRING_FAIL,
- NORMAL_MODE
- };
- CC1101 radio;
- #define MY_GATEWAY_SERIAL
- #define MY_BAUD_RATE 38400 // Do not change, limitation by frequency (crystal)
- //#define MY_DEBUG
- #include <MySensors.h>
- #define CHILD_ID_FAN 1
- #define CHILD_ID_CLONE 2
- #define CHILD_ID_TARGET_ADDRESS 3
- #define CHILD_ID_SOURCE_ADDRESS 4
- #define CHILD_ID_INDOOR_HUM 5
- #define CHILD_ID_OUTDOOR_HUM 6
- #define CHILD_ID_EXHAUST_TEMP 7
- #define CHILD_ID_SUPPLY_TEMP 8
- #define CHILD_ID_BYPASS_POS 9
- #define CHILD_ID_EXHAUST_FANSPEED 10
- #define CHILD_ID_SUPPLY_FANSPEED 11
- #define CHILD_ID_SUPPLY_FLOW 12
- #define CHILD_ID_EXHAUST_FLOW 13
- #define CHILD_ID_INDOOR_TEMP 14
- #define CHILD_ID_OUTDOOR_TEMP 15
- #define CHILD_ID_BYPASS_MODE 16
- #define CHILD_ID_DEMAND 17
- #define CHILD_ID_COMFORT_TEMP 18
- #define CHILD_ID_COMFORT_SETPOINT 19
- #define CHILD_ID_DEBUG 99
- #define SN "FanX"
- #define SV "1.19"
- // Comfort setpoint child is an S_DIMMER (light + V_PERCENTAGE slider) where the
- // percentage value IS the °C value (0..30). Retry of the SV 1.16 attempt on the
- // F()-optimised 1.18 base — see fanx_no_s_hvac memory.
- #define COMFORT_SETPOINT_MAX_C 30
- // HA-side bypass values shown on the V_PERCENTAGE dimmer (1..4).
- // Wire-side bytes (BYPASS_AUTO/OPEN/CLOSE/UNKNOWN) live in cc1101.h.
- #define HA_BYPASS_AUTO 1
- #define HA_BYPASS_OPEN 2
- #define HA_BYPASS_CLOSE 3
- #define HA_BYPASS_UNKNOWN 4
- MyMessage msgSourceAddress(CHILD_ID_TARGET_ADDRESS, V_VAR1);
- MyMessage msgTargetAddress(CHILD_ID_SOURCE_ADDRESS, V_VAR1);
- MyMessage msgCloneSwitch(CHILD_ID_CLONE, V_STATUS);
- MyMessage msgFANspeed(CHILD_ID_FAN, V_PERCENTAGE);
- MyMessage msgFANstate(CHILD_ID_FAN, V_STATUS);
- MyMessage msgIndoorHUM(CHILD_ID_INDOOR_HUM, V_HUM);
- MyMessage msgOutdoorHUM(CHILD_ID_OUTDOOR_HUM, V_HUM);
- MyMessage msgExhaustTEMP(CHILD_ID_EXHAUST_TEMP, V_TEMP);
- MyMessage msgSupplyTEMP(CHILD_ID_SUPPLY_TEMP, V_TEMP);
- MyMessage msgIndoorTEMP(CHILD_ID_INDOOR_TEMP, V_TEMP);
- MyMessage msgOutdoorTEMP(CHILD_ID_OUTDOOR_TEMP, V_TEMP);
- MyMessage msgBypassPOS(CHILD_ID_BYPASS_POS, V_VAR1);
- MyMessage msgExhaustFAN(CHILD_ID_EXHAUST_FANSPEED, V_VAR1);
- MyMessage msgSupplyFAN(CHILD_ID_SUPPLY_FANSPEED, V_VAR1);
- MyMessage msgSupplyFLOW(CHILD_ID_SUPPLY_FLOW, V_VAR1);
- MyMessage msgExhaustFLOW(CHILD_ID_EXHAUST_FLOW, V_VAR1);
- MyMessage msgBypassMODE_VAL(CHILD_ID_BYPASS_MODE, V_PERCENTAGE);
- MyMessage msgBypassMODE_STATE(CHILD_ID_BYPASS_MODE, V_STATUS);
- MyMessage msgDEMAND_PCT(CHILD_ID_DEMAND, V_PERCENTAGE);
- MyMessage msgDEMAND_STATE(CHILD_ID_DEMAND, V_STATUS);
- MyMessage msgComfortTEMP(CHILD_ID_COMFORT_TEMP, V_TEMP);
- MyMessage msgComfortPCT(CHILD_ID_COMFORT_SETPOINT, V_PERCENTAGE);
- MyMessage msgComfortSTATE(CHILD_ID_COMFORT_SETPOINT, V_STATUS);
- MyMessage msgDebug(CHILD_ID_DEBUG, V_TEXT);
- // Read/written only from loop() and receive(); receive() runs inline from the MySensors
- // dispatcher inside loop(), not from an ISR — so no volatile needed.
- uint8_t fanBypassModeRequest = BYPASS_UNKNOWN; // equal to currentFanState at boot
- int fanSpeedRequest = 1;
- int prevFanSpeed = 0;
- int16_t fanDemandRequest = -1;
- int16_t fanComfortTempRequest = -1; // centi-°C, -1 = no pending write
- static const unsigned long DEMAND_REBROADCAST_MS = 60000UL;
- ModuleState dongleState = JUST_BOOTED;
- //******************************************************************************************//
- // //
- // Utilities //
- // //
- //******************************************************************************************//
- // Generic debug message helper. Format string MUST be wrapped in F() so it stays
- // in flash; vsnprintf_P reads it from PROGMEM. Saves ~RAM-worth of string literals
- // across every call site. Payload truncated to MySensors' 25-char V_TEXT limit.
- // Usage: debugMsg(F("bp ok")); debugMsg(F("bp fail %u/%u"), n, TX_RETRY_COUNT);
- void debugMsg(const __FlashStringHelper *fmt, ...)
- {
- char buf[26];
- va_list args;
- va_start(args, fmt);
- vsnprintf_P(buf, sizeof(buf), (const char *)fmt, args);
- va_end(args);
- send(msgDebug.set(buf));
- }
- void ledFlash(uint8_t flashCount)
- {
- for (int i = 0; i < flashCount; i++)
- {
- digitalWrite(ONBOARD_LED, LOW); // RADIO LED ON
- wait(LED_FLASH_TIME * 2);
- digitalWrite(ONBOARD_LED, HIGH); // RADIO LED OFF
- wait(LED_FLASH_TIME * 2);
- }
- }
- // Decodes signed 16-bit centi-degrees-C into float degrees-C.
- float toCelsius(uint16_t raw)
- {
- return (int16_t)raw / 100.0f;
- }
- // Convert centi-litres/sec (Ramses wire units) to m³/h.
- float toM3h(uint16_t centiLps)
- {
- return (centiLps / 100.0f) * 3.6f;
- }
- // Largest formatted value: "63:262143" = 9 chars + NUL. 16 leaves margin.
- static void formatRamsesAddress(uint32_t addressWord, char *out, uint8_t outLength)
- {
- uint8_t deviceId = (addressWord & 0xFC0000) >> 18;
- uint32_t addressId = addressWord & 0x03FFFF;
- snprintf_P(out, outLength, PSTR("%u:%lu"), (unsigned)deviceId, (unsigned long)addressId);
- }
- //******************************************************************************************//
- // //
- // Transmit new state //
- // //
- //******************************************************************************************//
- void updateFanSpeed(int16_t speedLevel)
- {
- send(msgFANspeed.set(speedLevel));
- }
- void updateFanState(int16_t fanState)
- {
- send(msgFANstate.set(fanState));
- }
- void updateBypassMode(int16_t bypassMode)
- {
- send(msgBypassMODE_VAL.set(bypassMode));
- }
- void cloneModeEnded(void)
- {
- send(msgCloneSwitch.set(0));
- }
- void cloneModeStarted(void)
- {
- send(msgCloneSwitch.set(1));
- }
- void sendNewSourceAddressToGateway()
- {
- uint32_t sourceAddress = ((uint32_t)radio.newFanState.address[3]<<16) | ((uint32_t)radio.newFanState.address[4]<<8) | ((uint32_t)radio.newFanState.address[5]<<0);
- char buffer[16];
- formatRamsesAddress(sourceAddress, buffer, sizeof(buffer));
- send(msgSourceAddress.set(buffer));
- }
- void sendNewTargetAddressToGateway()
- {
- uint32_t targetAddress = ((uint32_t)radio.newFanState.address[0]<<16) | ((uint32_t)radio.newFanState.address[1]<<8) | ((uint32_t)radio.newFanState.address[2]<<0);
- char buffer[16];
- formatRamsesAddress(targetAddress, buffer, sizeof(buffer));
- send(msgTargetAddress.set(buffer));
- }
- //******************************************************************************************//
- // //
- // Update new params to controller //
- // //
- //******************************************************************************************//
- void updateNewParams()
- {
- if(radio.currentFanState.indoorHumidity != radio.newFanState.indoorHumidity)
- {
- send(msgIndoorHUM.set(radio.newFanState.indoorHumidity));
- radio.currentFanState.indoorHumidity = radio.newFanState.indoorHumidity;
- }
- if(radio.currentFanState.outdoorHumidity != radio.newFanState.outdoorHumidity)
- {
- send(msgOutdoorHUM.set(radio.newFanState.outdoorHumidity));
- radio.currentFanState.outdoorHumidity = radio.newFanState.outdoorHumidity;
- }
- if(radio.currentFanState.exhaustTemperature != radio.newFanState.exhaustTemperature)
- {
- send(msgExhaustTEMP.set(toCelsius(radio.newFanState.exhaustTemperature), 1));
- radio.currentFanState.exhaustTemperature = radio.newFanState.exhaustTemperature;
- }
- if(radio.currentFanState.supplyTemperature != radio.newFanState.supplyTemperature)
- {
- send(msgSupplyTEMP.set(toCelsius(radio.newFanState.supplyTemperature), 1));
- radio.currentFanState.supplyTemperature = radio.newFanState.supplyTemperature;
- }
- if(radio.currentFanState.indoorTemperature != radio.newFanState.indoorTemperature)
- {
- send(msgIndoorTEMP.set(toCelsius(radio.newFanState.indoorTemperature), 1));
- radio.currentFanState.indoorTemperature = radio.newFanState.indoorTemperature;
- }
- if(radio.currentFanState.outdoorTemperature != radio.newFanState.outdoorTemperature)
- {
- send(msgOutdoorTEMP.set(toCelsius(radio.newFanState.outdoorTemperature), 1));
- radio.currentFanState.outdoorTemperature = radio.newFanState.outdoorTemperature;
- }
- if(radio.currentFanState.bypassPosition != radio.newFanState.bypassPosition)
- {
- send(msgBypassPOS.set(radio.newFanState.bypassPosition / 2)); // 0..200 -> 0..100%
- radio.currentFanState.bypassPosition = radio.newFanState.bypassPosition;
- }
- if(radio.currentFanState.exhaustFanspeed != radio.newFanState.exhaustFanspeed)
- {
- send(msgExhaustFAN.set(radio.newFanState.exhaustFanspeed / 2)); // 0..200 -> 0..100%
- radio.currentFanState.exhaustFanspeed = radio.newFanState.exhaustFanspeed;
- }
- if(radio.currentFanState.supplyFanspeed != radio.newFanState.supplyFanspeed)
- {
- send(msgSupplyFAN.set(radio.newFanState.supplyFanspeed / 2)); // 0..200 -> 0..100%
- radio.currentFanState.supplyFanspeed = radio.newFanState.supplyFanspeed;
- }
- if(radio.currentFanState.supplyFlow != radio.newFanState.supplyFlow)
- {
- send(msgSupplyFLOW.set(toM3h(radio.newFanState.supplyFlow), 1));
- radio.currentFanState.supplyFlow = radio.newFanState.supplyFlow;
- }
- if(radio.currentFanState.exhaustFlow != radio.newFanState.exhaustFlow)
- {
- send(msgExhaustFLOW.set(toM3h(radio.newFanState.exhaustFlow), 1));
- radio.currentFanState.exhaustFlow = radio.newFanState.exhaustFlow;
- }
- }
- //******************************************************************************************//
- // //
- // State handlers //
- // //
- //******************************************************************************************//
- // Periodic poll timer is reset after pairing (handlePairingMode below) to
- // avoid an immediate burst, so this is file-scope rather than function-static.
- static unsigned long prevPollMs = 0;
- // Returns true if it handled a pending fan-speed change this tick.
- // 22F1 from REM is fire-and-forget by spec (ramses.py:1148: REM sends only I,
- // FAN replies RP only to RQ — and only CO2 sensors send RQ 22F1). So we trust
- // the TX once transmitData reports a valid bus exchange, instead of waiting
- // for a 22F1 echo that never comes. The 5-retry / force-match path only kicks
- // in if transmitData itself keeps failing (no BOF / CRC / address).
- static bool handleFanSpeedRequest()
- {
- static uint8_t retryCount = 0;
- if (radio.currentFanState.fanSpeed == fanSpeedRequest)
- {
- retryCount = 0;
- return false;
- }
- if (retryCount >= TX_RETRY_COUNT)
- {
- debugMsg(F("speed forced %d!=%d"), (int)radio.currentFanState.fanSpeed, (int)fanSpeedRequest);
- radio.currentFanState.fanSpeed = fanSpeedRequest;
- retryCount = 0;
- return true;
- }
- if (radio.txFanspeed(fanSpeedRequest))
- {
- ledFlash(1);
- radio.currentFanState.fanSpeed = fanSpeedRequest; // trust the TX; no 22F1 echo expected
- retryCount = 0;
- return true;
- }
- debugMsg(F("speed fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
- retryCount++;
- return true;
- }
- // Returns true if it handled a pending bypass change this tick.
- static bool handleBypassRequest()
- {
- static uint8_t retryCount = 0;
- if (radio.currentFanState.bypassMode == fanBypassModeRequest)
- {
- retryCount = 0;
- return false;
- }
- if (retryCount >= TX_RETRY_COUNT)
- {
- radio.currentFanState.bypassMode = fanBypassModeRequest;
- retryCount = 0;
- debugMsg(F("bypass forced"));
- return true;
- }
- if (radio.setBypass(fanBypassModeRequest))
- {
- ledFlash(1);
- radio.currentFanState.bypassMode = radio.newFanState.bypassMode;
- }
- else
- {
- debugMsg(F("bypass fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
- }
- // See handleFanSpeedRequest: count attempts, not failures. The fan does not
- // reliably echo a 22F7 back, so newFanState.bypassMode stays stale and we'd
- // loop forever on success otherwise — starving demand and pollFanState.
- retryCount++;
- return true;
- }
- // Push an observed fan-speed change out to HA, tracking off/on transitions.
- static void processFanSpeedChange()
- {
- uint8_t newSpeed = radio.newFanState.fanSpeed;
- bool wasOff = (radio.currentFanState.fanSpeed == 0);
- bool turningOff = (newSpeed == 0);
- if (turningOff)
- prevFanSpeed = radio.currentFanState.fanSpeed; // remember to restore on next "on"
- radio.currentFanState.fanSpeed = newSpeed;
- fanSpeedRequest = newSpeed; // prevent RF update next cycle
- if (turningOff)
- updateFanState(0);
- else
- {
- if (wasOff) updateFanState(1); // off -> on transition: send V_STATUS once
- updateFanSpeed(newSpeed);
- }
- }
- // Process the freshly-polled fan state. First call seeds HA-side state;
- // subsequent calls push telemetry deltas and observed speed changes.
- static void processFanState()
- {
- static bool firstRun = true;
- if (firstRun)
- {
- firstRun = false;
- radio.currentFanState.fanSpeed = radio.newFanState.fanSpeed;
- fanSpeedRequest = radio.currentFanState.fanSpeed; // prevent RF update next cycle
- updateFanState(radio.currentFanState.fanSpeed == 0 ? 0 : 1);
- updateFanSpeed(radio.currentFanState.fanSpeed);
- return;
- }
- updateNewParams();
- if (radio.currentFanState.fanSpeed != radio.newFanState.fanSpeed)
- processFanSpeedChange();
- }
- static void pollFanState()
- {
- static uint8_t consecFails = 0;
- const uint8_t POLL_FAIL_THRESHOLD = 3;
- unsigned long now = millis();
- if ((now - prevPollMs) <= FAN_INTERVAL) return;
- prevPollMs = now; // anchor; no catch-up burst if a previous poll was starved
- if (!radio.requestFanState())
- {
- if (++consecFails == POLL_FAIL_THRESHOLD) debugMsg(F("poll fail"));
- return;
- }
- consecFails = 0;
- ledFlash(1);
- processFanState();
- }
- // Returns true if it consumed this tick (attempted a TX or gave up the retry budget).
- static bool handleDemandRequest()
- {
- static uint8_t retryCount = 0;
- static unsigned long lastAttemptMs = 0;
- // 0xFF (out-of-band for a 0..100 pct) so first HA write of 0 still produces changed==true.
- static uint8_t currentPct = 0xFF;
- if (fanDemandRequest < 0) return false;
- // 0..100 -> 0..0xFF (8-byte Orcon 31E0 form). Fan-side curve clips to ~44..89.5%.
- uint8_t demandByte = (uint8_t)((uint16_t)fanDemandRequest * 255 / 100);
- unsigned long now = millis();
- bool changed = (currentPct != (uint8_t)fanDemandRequest);
- bool stale = (now - lastAttemptMs) >= DEMAND_REBROADCAST_MS;
- if (!changed && !stale) return false;
- // Anchor before TX so a busy transmitData doesn't refire 'stale' next iter.
- // Note: this is "last attempt", not "last successful TX".
- lastAttemptMs = now;
- if (retryCount >= TX_RETRY_COUNT)
- {
- // Give up this session — sync state so we don't refire 'changed' every loop.
- currentPct = (uint8_t)fanDemandRequest;
- retryCount = 0;
- debugMsg(F("demand forced"));
- return true;
- }
- if (radio.txDemand(demandByte))
- {
- ledFlash(1);
- currentPct = (uint8_t)fanDemandRequest;
- retryCount = 0;
- }
- else
- {
- debugMsg(F("demand fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
- retryCount++;
- }
- return true;
- }
- // Handle a pending comfort-temperature write from HA. Pattern matches handleBypassRequest:
- // count attempts (not failures) so a transmitData success on a coincident 31DA — which
- // doesn't update newFanState.comfortTemperature — still trips the force budget instead
- // of looping forever.
- static bool handleComfortTempRequest()
- {
- static uint8_t retryCount = 0;
- if (fanComfortTempRequest < 0) return false;
- uint16_t targetCenti = (uint16_t)fanComfortTempRequest;
- if (radio.currentFanState.comfortTemperature == targetCenti)
- {
- fanComfortTempRequest = -1;
- retryCount = 0;
- return false;
- }
- if (retryCount >= TX_RETRY_COUNT)
- {
- radio.currentFanState.comfortTemperature = targetCenti;
- send(msgComfortTEMP.set(toCelsius(targetCenti), 1));
- send(msgComfortPCT.set((int)(targetCenti / 100)));
- fanComfortTempRequest = -1;
- retryCount = 0;
- debugMsg(F("comfort set forced"));
- return true;
- }
- if (radio.setComfortTemperature(targetCenti))
- {
- ledFlash(1);
- if (radio.currentFanState.comfortTemperature != radio.newFanState.comfortTemperature)
- {
- radio.currentFanState.comfortTemperature = radio.newFanState.comfortTemperature;
- send(msgComfortTEMP.set(toCelsius(radio.currentFanState.comfortTemperature), 1));
- send(msgComfortPCT.set((int)(radio.currentFanState.comfortTemperature / 100)));
- }
- }
- else
- {
- debugMsg(F("comfort set fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
- }
- retryCount++;
- return true;
- }
- // Periodic poll of the Orcon "Comfort temperature" (2411 param 0x75 — the bypass target).
- // Fires once on boot (nextPollMs starts at 0) and then every COMFORT_TEMP_POLL_INTERVAL_MS.
- // Returns true if it consumed this tick.
- static bool handleComfortTempPoll()
- {
- static uint8_t retryCount = 0;
- static unsigned long nextPollMs = 0;
- unsigned long now = millis();
- if ((long)(now - nextPollMs) < 0) return false;
- if (retryCount >= TX_RETRY_COUNT)
- {
- debugMsg(F("comfort forced"));
- retryCount = 0;
- nextPollMs = now + COMFORT_TEMP_POLL_INTERVAL_MS;
- return true;
- }
- if (radio.requestComfortTemperature())
- {
- ledFlash(1);
- retryCount = 0;
- nextPollMs = now + COMFORT_TEMP_POLL_INTERVAL_MS;
- // Publish inline. processFanState() skips updateNewParams() on its first run, so
- // routing the diff through there would delay the post-boot value by an extra 5s.
- if (radio.currentFanState.comfortTemperature != radio.newFanState.comfortTemperature)
- {
- send(msgComfortTEMP.set(toCelsius(radio.newFanState.comfortTemperature), 1));
- send(msgComfortPCT.set((int)(radio.newFanState.comfortTemperature / 100)));
- radio.currentFanState.comfortTemperature = radio.newFanState.comfortTemperature;
- }
- }
- else
- {
- debugMsg(F("comfort fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
- retryCount++;
- nextPollMs = now + COMFORT_TEMP_RETRY_INTERVAL_MS;
- }
- return true;
- }
- static void handleJustBooted()
- {
- ledFlash(1);
- send(msgSourceAddress.set(0));
- send(msgTargetAddress.set(0));
- send(msgCloneSwitch.set(0));
- send(msgFANspeed.set(radio.currentFanState.fanSpeed));
- send(msgFANstate.set(radio.currentFanState.fanSpeed == 0 ? 0 : 1));
- send(msgBypassMODE_VAL.set(HA_BYPASS_UNKNOWN));
- send(msgBypassMODE_STATE.set(1)); // On
- send(msgDEMAND_PCT.set(0));
- send(msgDEMAND_STATE.set(0));
- send(msgComfortTEMP.set(toCelsius(radio.currentFanState.comfortTemperature), 1));
- send(msgComfortSTATE.set(1)); // S_DIMMER stays "on"; the % value carries the °C setpoint
- send(msgComfortPCT.set((int)(radio.currentFanState.comfortTemperature / 100)));
- if ((radio.readReg(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) & 0x1f) != 1)
- {
- wait(1000);
- return;
- }
- // Radio is alive — check EEPROM for a paired address.
- bool emptyEeprom = true;
- for (int i = 0; i < 6; i++)
- {
- radio.newFanState.address[i] = loadState(i);
- if (radio.newFanState.address[i] != 0xFF) emptyEeprom = false;
- }
- if (emptyEeprom)
- {
- dongleState = PAIRING_FAIL;
- return;
- }
- sendNewSourceAddressToGateway();
- sendNewTargetAddressToGateway();
- dongleState = NORMAL_MODE;
- debugMsg(F("boot ok v" SV));
- }
- static void handlePairingMode()
- {
- digitalWrite(ONBOARD_LED, LOW); // RADIO LED ON
- radio.setRxMode();
- if (radio.cloneMode())
- {
- for (int i = 0; i < 6; i++)
- saveState(i, radio.newFanState.address[i]);
- sendNewSourceAddressToGateway();
- sendNewTargetAddressToGateway();
- prevPollMs = millis(); // prevent immediate burst on entering NORMAL_MODE
- dongleState = NORMAL_MODE;
- debugMsg(F("paired v" SV));
- }
- else
- dongleState = PAIRING_FAIL;
- digitalWrite(ONBOARD_LED, HIGH); // RADIO LED OFF
- cloneModeEnded();
- }
- //******************************************************************************************//
- // //
- // Init //
- // //
- //******************************************************************************************//
- void before()
- {
- radio.init();
- }
- void setup()
- {
- wdt_disable();
- pinMode(ONBOARD_LED, OUTPUT);
- digitalWrite(ONBOARD_LED, HIGH);
- Serial1.begin(38400); // used for transmitting data to FAN
- while (!Serial1);
- radio.currentFanState.fanSpeed = 1;
- radio.currentFanState.bypassMode = BYPASS_UNKNOWN; // matches fanBypassModeRequest so we don't TX before a real value arrives
- wdt_enable(WDTO_8S);
- }
- void presentation()
- {
- sendSketchInfo(F(SN), F(SV));
- present(CHILD_ID_FAN, S_DIMMER, F("FAN speed"));
- present(CHILD_ID_CLONE, S_BINARY, F("Clone switch"));
- present(CHILD_ID_TARGET_ADDRESS, S_CUSTOM, F("Target address"));
- present(CHILD_ID_SOURCE_ADDRESS, S_CUSTOM, F("Source address"));
- present(CHILD_ID_INDOOR_HUM, S_HUM, F("Indoor humidity (%)"));
- present(CHILD_ID_OUTDOOR_HUM, S_HUM, F("Outdoor humidity (%)"));
- present(CHILD_ID_EXHAUST_TEMP, S_TEMP, F("Exhaust temperature (ºC)"));
- present(CHILD_ID_SUPPLY_TEMP, S_TEMP, F("Supply temperature (ºC)"));
- present(CHILD_ID_BYPASS_POS, S_CUSTOM, F("Bypass position (%)"));
- present(CHILD_ID_EXHAUST_FANSPEED, S_CUSTOM, F("Exhaust FAN (%)"));
- present(CHILD_ID_SUPPLY_FANSPEED, S_CUSTOM, F("Supply FAN (%)"));
- present(CHILD_ID_SUPPLY_FLOW, S_CUSTOM, F("Supply flow (m3/h)"));
- present(CHILD_ID_EXHAUST_FLOW, S_CUSTOM, F("Exhaust flow (m3/h)"));
- present(CHILD_ID_INDOOR_TEMP, S_TEMP, F("Indoor temperature (ºC)"));
- present(CHILD_ID_OUTDOOR_TEMP, S_TEMP, F("Outdoor temperature (ºC)"));
- present(CHILD_ID_BYPASS_MODE, S_DIMMER, F("Bypass mode (1 , 2, 3)"));
- present(CHILD_ID_DEMAND, S_DIMMER, F("Fan demand"));
- present(CHILD_ID_COMFORT_TEMP, S_TEMP, F("Comfort Temperature"));
- present(CHILD_ID_COMFORT_SETPOINT, S_DIMMER, F("Comfort Setpoint"));
- present(CHILD_ID_DEBUG, S_INFO, F("Debug status"));
- }
- //******************************************************************************************//
- // //
- // MAIN //
- // //
- //******************************************************************************************//
- void loop()
- {
- switch (dongleState)
- {
- case JUST_BOOTED:
- handleJustBooted();
- break;
- case RF15_PAIRINGSMODE:
- handlePairingMode();
- break;
- case PAIRING_FAIL:
- ledFlash(2);
- wait(750);
- break;
- case NORMAL_MODE:
- // At most one command per tick (shared radio, priority order).
- // If no request was pending, fall back to polling fan state.
- if (handleFanSpeedRequest()) break;
- if (handleBypassRequest()) break;
- if (handleDemandRequest()) break;
- if (handleComfortTempRequest()) break;
- if (handleComfortTempPoll()) break;
- pollFanState();
- break;
- }
- wdt_reset();
- }
- //******************************************************************************************//
- // //
- // Receive from MySensors //
- // //
- //******************************************************************************************//
- void receive(const MyMessage &message)
- {
- if(message.isAck())
- {
- return;
- }
- if( (message.getType() == V_STATUS) && (message.getSensor() == CHILD_ID_CLONE) )
- {
- if(message.getBool())
- {
- dongleState = RF15_PAIRINGSMODE;
- cloneModeStarted();
- }
- }
- if(message.getSensor() == CHILD_ID_FAN)
- {
- if(message.getType() == V_STATUS)
- {
- if(!message.getBool())
- {
- // Remember speed so a bare V_STATUS=on (no brightness) can restore it.
- prevFanSpeed = radio.currentFanState.fanSpeed;
- fanSpeedRequest = 0;
- updateFanState(0);
- }
- else
- {
- fanSpeedRequest = prevFanSpeed;
- updateFanState(1);
- }
- }
- if (message.getType() == V_PERCENTAGE)
- {
- fanSpeedRequest = constrain(message.getInt(), 0, 4);
- updateFanSpeed(fanSpeedRequest);
- }
- }
- if(message.getSensor() == CHILD_ID_BYPASS_MODE)
- {
- if (message.getType() == V_PERCENTAGE)
- {
- int16_t haBypassValue = constrain(message.getInt(), 1, 3);
- updateBypassMode(haBypassValue);
- switch (haBypassValue)
- {
- case HA_BYPASS_AUTO: fanBypassModeRequest = BYPASS_AUTO; break;
- case HA_BYPASS_OPEN: fanBypassModeRequest = BYPASS_OPEN; break;
- case HA_BYPASS_CLOSE: fanBypassModeRequest = BYPASS_CLOSE; break;
- default: break;
- }
- }
- }
- if (message.getSensor() == CHILD_ID_DEMAND)
- {
- if (message.getType() == V_PERCENTAGE)
- {
- fanDemandRequest = constrain(message.getInt(), 0, 100);
- }
- else if (message.getType() == V_STATUS)
- {
- // HA "light off" -> demand 0. Bare "on" (no brightness) is ignored:
- // there's no meaningful previous-value to restore for a continuous setpoint.
- if (!message.getBool()) fanDemandRequest = 0;
- }
- }
- if (message.getSensor() == CHILD_ID_COMFORT_SETPOINT)
- {
- if (message.getType() == V_PERCENTAGE)
- {
- // The slider's % value IS the °C setpoint (0..COMFORT_SETPOINT_MAX_C).
- int pct = constrain(message.getInt(), 0, COMFORT_SETPOINT_MAX_C);
- fanComfortTempRequest = (int16_t)(pct * 100);
- send(msgComfortPCT.set(pct));
- }
- else if (message.getType() == V_STATUS)
- {
- // S_DIMMER also accepts on/off. Treat both as no-op: there's no "off"
- // for a setpoint, and bare "on" carries no value to apply.
- send(msgComfortSTATE.set(1));
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment