Guest User

mysensors.ino

a guest
Jun 2nd, 2026
61
0
53 days
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C 27.85 KB | None | 0 0
  1. #include <Arduino.h>
  2. #include <avr/wdt.h>
  3. #include <stdarg.h>
  4. #include <stdio.h>
  5. #include "cc1101.h"
  6. #define ONBOARD_LED               7
  7. #define FAN_INTERVAL              5000  // RF link interval
  8. #define LED_FLASH_TIME            25  // ms
  9. #define TX_RETRY_COUNT              5
  10. #define COMFORT_TEMP_POLL_INTERVAL_MS  300000UL  // 5 minutes between successful polls
  11. #define COMFORT_TEMP_RETRY_INTERVAL_MS  30000UL  // 30 s between failed retries — spaces out blocking transmitData calls so pollFanState etc. get turns in between
  12.  
  13. enum ModuleState
  14. {
  15.   JUST_BOOTED,
  16.   RF15_PAIRINGSMODE,
  17.   PAIRING_FAIL,
  18.   NORMAL_MODE
  19. };
  20.  
  21. CC1101 radio;
  22.  
  23. #define MY_GATEWAY_SERIAL
  24. #define MY_BAUD_RATE                38400 // Do not change, limitation by frequency (crystal)
  25.  
  26. //#define MY_DEBUG
  27. #include <MySensors.h>
  28. #define CHILD_ID_FAN                1
  29. #define CHILD_ID_CLONE              2
  30. #define CHILD_ID_TARGET_ADDRESS     3
  31. #define CHILD_ID_SOURCE_ADDRESS     4
  32.  
  33. #define CHILD_ID_INDOOR_HUM         5
  34. #define CHILD_ID_OUTDOOR_HUM        6
  35. #define CHILD_ID_EXHAUST_TEMP       7
  36. #define CHILD_ID_SUPPLY_TEMP        8
  37. #define CHILD_ID_BYPASS_POS         9
  38. #define CHILD_ID_EXHAUST_FANSPEED   10
  39. #define CHILD_ID_SUPPLY_FANSPEED    11
  40. #define CHILD_ID_SUPPLY_FLOW        12
  41. #define CHILD_ID_EXHAUST_FLOW       13
  42. #define CHILD_ID_INDOOR_TEMP        14
  43. #define CHILD_ID_OUTDOOR_TEMP       15
  44. #define CHILD_ID_BYPASS_MODE        16
  45. #define CHILD_ID_DEMAND             17
  46. #define CHILD_ID_COMFORT_TEMP       18
  47. #define CHILD_ID_COMFORT_SETPOINT   19
  48. #define CHILD_ID_DEBUG              99
  49.  
  50. #define SN                          "FanX"
  51. #define SV                          "1.19"
  52.  
  53. // Comfort setpoint child is an S_DIMMER (light + V_PERCENTAGE slider) where the
  54. // percentage value IS the °C value (0..30). Retry of the SV 1.16 attempt on the
  55. // F()-optimised 1.18 base — see fanx_no_s_hvac memory.
  56. #define COMFORT_SETPOINT_MAX_C      30
  57.  
  58. // HA-side bypass values shown on the V_PERCENTAGE dimmer (1..4).
  59. // Wire-side bytes (BYPASS_AUTO/OPEN/CLOSE/UNKNOWN) live in cc1101.h.
  60. #define HA_BYPASS_AUTO     1
  61. #define HA_BYPASS_OPEN     2
  62. #define HA_BYPASS_CLOSE    3
  63. #define HA_BYPASS_UNKNOWN  4
  64.  
  65. MyMessage msgSourceAddress(CHILD_ID_TARGET_ADDRESS, V_VAR1);
  66. MyMessage msgTargetAddress(CHILD_ID_SOURCE_ADDRESS, V_VAR1);
  67. MyMessage msgCloneSwitch(CHILD_ID_CLONE, V_STATUS);
  68. MyMessage msgFANspeed(CHILD_ID_FAN, V_PERCENTAGE);
  69. MyMessage msgFANstate(CHILD_ID_FAN, V_STATUS);
  70.  
  71. MyMessage msgIndoorHUM(CHILD_ID_INDOOR_HUM, V_HUM);
  72. MyMessage msgOutdoorHUM(CHILD_ID_OUTDOOR_HUM, V_HUM);
  73.  
  74. MyMessage msgExhaustTEMP(CHILD_ID_EXHAUST_TEMP, V_TEMP);
  75. MyMessage msgSupplyTEMP(CHILD_ID_SUPPLY_TEMP, V_TEMP);
  76.  
  77. MyMessage msgIndoorTEMP(CHILD_ID_INDOOR_TEMP, V_TEMP);
  78. MyMessage msgOutdoorTEMP(CHILD_ID_OUTDOOR_TEMP, V_TEMP);
  79.  
  80. MyMessage msgBypassPOS(CHILD_ID_BYPASS_POS, V_VAR1);
  81.  
  82. MyMessage msgExhaustFAN(CHILD_ID_EXHAUST_FANSPEED, V_VAR1);
  83. MyMessage msgSupplyFAN(CHILD_ID_SUPPLY_FANSPEED, V_VAR1);
  84.  
  85. MyMessage msgSupplyFLOW(CHILD_ID_SUPPLY_FLOW, V_VAR1);
  86. MyMessage msgExhaustFLOW(CHILD_ID_EXHAUST_FLOW, V_VAR1);
  87.  
  88. MyMessage msgBypassMODE_VAL(CHILD_ID_BYPASS_MODE, V_PERCENTAGE);
  89. MyMessage msgBypassMODE_STATE(CHILD_ID_BYPASS_MODE, V_STATUS);
  90.  
  91. MyMessage msgDEMAND_PCT(CHILD_ID_DEMAND, V_PERCENTAGE);
  92. MyMessage msgDEMAND_STATE(CHILD_ID_DEMAND, V_STATUS);
  93.  
  94. MyMessage msgComfortTEMP(CHILD_ID_COMFORT_TEMP, V_TEMP);
  95. MyMessage msgComfortPCT(CHILD_ID_COMFORT_SETPOINT, V_PERCENTAGE);
  96. MyMessage msgComfortSTATE(CHILD_ID_COMFORT_SETPOINT, V_STATUS);
  97.  
  98. MyMessage msgDebug(CHILD_ID_DEBUG, V_TEXT);
  99.  
  100. // Read/written only from loop() and receive(); receive() runs inline from the MySensors
  101. // dispatcher inside loop(), not from an ISR — so no volatile needed.
  102. uint8_t fanBypassModeRequest = BYPASS_UNKNOWN; // equal to currentFanState at boot
  103. int fanSpeedRequest = 1;
  104. int prevFanSpeed = 0;
  105. int16_t fanDemandRequest = -1;
  106. int16_t fanComfortTempRequest = -1;  // centi-°C, -1 = no pending write
  107. static const unsigned long DEMAND_REBROADCAST_MS = 60000UL;
  108.  
  109. ModuleState dongleState = JUST_BOOTED;
  110.  
  111. //******************************************************************************************//
  112. //                                                                                          //
  113. //                        Utilities                                                         //
  114. //                                                                                          //
  115. //******************************************************************************************//
  116.  
  117. // Generic debug message helper. Format string MUST be wrapped in F() so it stays
  118. // in flash; vsnprintf_P reads it from PROGMEM. Saves ~RAM-worth of string literals
  119. // across every call site. Payload truncated to MySensors' 25-char V_TEXT limit.
  120. // Usage: debugMsg(F("bp ok")); debugMsg(F("bp fail %u/%u"), n, TX_RETRY_COUNT);
  121. void debugMsg(const __FlashStringHelper *fmt, ...)
  122. {
  123.   char buf[26];
  124.   va_list args;
  125.   va_start(args, fmt);
  126.   vsnprintf_P(buf, sizeof(buf), (const char *)fmt, args);
  127.   va_end(args);
  128.   send(msgDebug.set(buf));
  129. }
  130.  
  131. void ledFlash(uint8_t flashCount)
  132. {
  133.   for (int i = 0; i < flashCount; i++)
  134.   {
  135.     digitalWrite(ONBOARD_LED, LOW);  // RADIO LED ON
  136.     wait(LED_FLASH_TIME * 2);
  137.     digitalWrite(ONBOARD_LED, HIGH); // RADIO LED OFF
  138.     wait(LED_FLASH_TIME * 2);
  139.   }
  140. }
  141.  
  142. // Decodes signed 16-bit centi-degrees-C into float degrees-C.
  143. float toCelsius(uint16_t raw)
  144. {
  145.   return (int16_t)raw / 100.0f;
  146. }
  147.  
  148. // Convert centi-litres/sec (Ramses wire units) to m³/h.
  149. float toM3h(uint16_t centiLps)
  150. {
  151.   return (centiLps / 100.0f) * 3.6f;
  152. }
  153.  
  154. // Largest formatted value: "63:262143" = 9 chars + NUL. 16 leaves margin.
  155. static void formatRamsesAddress(uint32_t addressWord, char *out, uint8_t outLength)
  156. {
  157.   uint8_t deviceId = (addressWord & 0xFC0000) >> 18;
  158.   uint32_t addressId = addressWord & 0x03FFFF;
  159.   snprintf_P(out, outLength, PSTR("%u:%lu"), (unsigned)deviceId, (unsigned long)addressId);
  160. }
  161.  
  162. //******************************************************************************************//
  163. //                                                                                          //
  164. //                        Transmit new state                                                //
  165. //                                                                                          //
  166. //******************************************************************************************//
  167.  
  168. void updateFanSpeed(int16_t speedLevel)
  169. {
  170.   send(msgFANspeed.set(speedLevel));
  171. }
  172.  
  173. void updateFanState(int16_t fanState)
  174. {
  175.   send(msgFANstate.set(fanState));
  176. }
  177.  
  178. void updateBypassMode(int16_t bypassMode)
  179. {
  180.   send(msgBypassMODE_VAL.set(bypassMode));
  181. }
  182.  
  183. void cloneModeEnded(void)
  184. {
  185.   send(msgCloneSwitch.set(0));
  186. }
  187.  
  188. void cloneModeStarted(void)
  189. {
  190.   send(msgCloneSwitch.set(1));
  191. }
  192.  
  193. void sendNewSourceAddressToGateway()
  194. {
  195.   uint32_t sourceAddress = ((uint32_t)radio.newFanState.address[3]<<16) | ((uint32_t)radio.newFanState.address[4]<<8) | ((uint32_t)radio.newFanState.address[5]<<0);
  196.   char buffer[16];
  197.   formatRamsesAddress(sourceAddress, buffer, sizeof(buffer));
  198.   send(msgSourceAddress.set(buffer));
  199. }
  200.  
  201. void sendNewTargetAddressToGateway()
  202. {
  203.   uint32_t targetAddress = ((uint32_t)radio.newFanState.address[0]<<16) | ((uint32_t)radio.newFanState.address[1]<<8) | ((uint32_t)radio.newFanState.address[2]<<0);
  204.   char buffer[16];
  205.   formatRamsesAddress(targetAddress, buffer, sizeof(buffer));
  206.   send(msgTargetAddress.set(buffer));
  207. }
  208.  
  209. //******************************************************************************************//
  210. //                                                                                          //
  211. //                        Update new params to controller                                   //
  212. //                                                                                          //
  213. //******************************************************************************************//
  214.  
  215. void updateNewParams()
  216. {
  217.   if(radio.currentFanState.indoorHumidity != radio.newFanState.indoorHumidity)
  218.   {
  219.     send(msgIndoorHUM.set(radio.newFanState.indoorHumidity));
  220.     radio.currentFanState.indoorHumidity = radio.newFanState.indoorHumidity;
  221.   }
  222.   if(radio.currentFanState.outdoorHumidity != radio.newFanState.outdoorHumidity)
  223.   {
  224.     send(msgOutdoorHUM.set(radio.newFanState.outdoorHumidity));
  225.     radio.currentFanState.outdoorHumidity = radio.newFanState.outdoorHumidity;
  226.   }
  227.   if(radio.currentFanState.exhaustTemperature != radio.newFanState.exhaustTemperature)
  228.   {
  229.     send(msgExhaustTEMP.set(toCelsius(radio.newFanState.exhaustTemperature), 1));
  230.     radio.currentFanState.exhaustTemperature = radio.newFanState.exhaustTemperature;
  231.   }
  232.   if(radio.currentFanState.supplyTemperature != radio.newFanState.supplyTemperature)
  233.   {
  234.     send(msgSupplyTEMP.set(toCelsius(radio.newFanState.supplyTemperature), 1));
  235.     radio.currentFanState.supplyTemperature = radio.newFanState.supplyTemperature;
  236.   }
  237.   if(radio.currentFanState.indoorTemperature != radio.newFanState.indoorTemperature)
  238.   {
  239.     send(msgIndoorTEMP.set(toCelsius(radio.newFanState.indoorTemperature), 1));
  240.     radio.currentFanState.indoorTemperature = radio.newFanState.indoorTemperature;
  241.   }
  242.   if(radio.currentFanState.outdoorTemperature != radio.newFanState.outdoorTemperature)
  243.   {
  244.     send(msgOutdoorTEMP.set(toCelsius(radio.newFanState.outdoorTemperature), 1));
  245.     radio.currentFanState.outdoorTemperature = radio.newFanState.outdoorTemperature;
  246.   }
  247.   if(radio.currentFanState.bypassPosition != radio.newFanState.bypassPosition)
  248.   {
  249.     send(msgBypassPOS.set(radio.newFanState.bypassPosition / 2)); // 0..200 -> 0..100%
  250.     radio.currentFanState.bypassPosition = radio.newFanState.bypassPosition;
  251.   }
  252.   if(radio.currentFanState.exhaustFanspeed != radio.newFanState.exhaustFanspeed)
  253.   {
  254.     send(msgExhaustFAN.set(radio.newFanState.exhaustFanspeed / 2)); //  0..200 -> 0..100%
  255.     radio.currentFanState.exhaustFanspeed = radio.newFanState.exhaustFanspeed;
  256.   }
  257.   if(radio.currentFanState.supplyFanspeed != radio.newFanState.supplyFanspeed)
  258.   {
  259.     send(msgSupplyFAN.set(radio.newFanState.supplyFanspeed / 2)); // 0..200 -> 0..100%
  260.     radio.currentFanState.supplyFanspeed = radio.newFanState.supplyFanspeed;
  261.   }
  262.   if(radio.currentFanState.supplyFlow != radio.newFanState.supplyFlow)
  263.   {
  264.     send(msgSupplyFLOW.set(toM3h(radio.newFanState.supplyFlow), 1));
  265.     radio.currentFanState.supplyFlow = radio.newFanState.supplyFlow;
  266.   }
  267.   if(radio.currentFanState.exhaustFlow != radio.newFanState.exhaustFlow)
  268.   {
  269.     send(msgExhaustFLOW.set(toM3h(radio.newFanState.exhaustFlow), 1));
  270.     radio.currentFanState.exhaustFlow = radio.newFanState.exhaustFlow;
  271.   }
  272. }
  273.  
  274. //******************************************************************************************//
  275. //                                                                                          //
  276. //                        State handlers                                                    //
  277. //                                                                                          //
  278. //******************************************************************************************//
  279.  
  280. // Periodic poll timer is reset after pairing (handlePairingMode below) to
  281. // avoid an immediate burst, so this is file-scope rather than function-static.
  282. static unsigned long prevPollMs = 0;
  283.  
  284. // Returns true if it handled a pending fan-speed change this tick.
  285. // 22F1 from REM is fire-and-forget by spec (ramses.py:1148: REM sends only I,
  286. // FAN replies RP only to RQ — and only CO2 sensors send RQ 22F1). So we trust
  287. // the TX once transmitData reports a valid bus exchange, instead of waiting
  288. // for a 22F1 echo that never comes. The 5-retry / force-match path only kicks
  289. // in if transmitData itself keeps failing (no BOF / CRC / address).
  290. static bool handleFanSpeedRequest()
  291. {
  292.   static uint8_t retryCount = 0;
  293.  
  294.   if (radio.currentFanState.fanSpeed == fanSpeedRequest)
  295.   {
  296.     retryCount = 0;
  297.     return false;
  298.   }
  299.  
  300.   if (retryCount >= TX_RETRY_COUNT)
  301.   {
  302.     debugMsg(F("speed forced %d!=%d"), (int)radio.currentFanState.fanSpeed, (int)fanSpeedRequest);
  303.     radio.currentFanState.fanSpeed = fanSpeedRequest;
  304.     retryCount = 0;
  305.     return true;
  306.   }
  307.  
  308.   if (radio.txFanspeed(fanSpeedRequest))
  309.   {
  310.     ledFlash(1);
  311.     radio.currentFanState.fanSpeed = fanSpeedRequest;  // trust the TX; no 22F1 echo expected
  312.     retryCount = 0;
  313.     return true;
  314.   }
  315.  
  316.   debugMsg(F("speed fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
  317.   retryCount++;
  318.   return true;
  319. }
  320.  
  321. // Returns true if it handled a pending bypass change this tick.
  322. static bool handleBypassRequest()
  323. {
  324.   static uint8_t retryCount = 0;
  325.  
  326.   if (radio.currentFanState.bypassMode == fanBypassModeRequest)
  327.   {
  328.     retryCount = 0;
  329.     return false;
  330.   }
  331.  
  332.   if (retryCount >= TX_RETRY_COUNT)
  333.   {
  334.     radio.currentFanState.bypassMode = fanBypassModeRequest;
  335.     retryCount = 0;
  336.     debugMsg(F("bypass forced"));
  337.     return true;
  338.   }
  339.  
  340.   if (radio.setBypass(fanBypassModeRequest))
  341.   {
  342.     ledFlash(1);
  343.     radio.currentFanState.bypassMode = radio.newFanState.bypassMode;
  344.   }
  345.   else
  346.   {
  347.     debugMsg(F("bypass fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
  348.   }
  349.   // See handleFanSpeedRequest: count attempts, not failures. The fan does not
  350.   // reliably echo a 22F7 back, so newFanState.bypassMode stays stale and we'd
  351.   // loop forever on success otherwise — starving demand and pollFanState.
  352.   retryCount++;
  353.   return true;
  354. }
  355.  
  356. // Push an observed fan-speed change out to HA, tracking off/on transitions.
  357. static void processFanSpeedChange()
  358. {
  359.   uint8_t newSpeed = radio.newFanState.fanSpeed;
  360.   bool wasOff = (radio.currentFanState.fanSpeed == 0);
  361.   bool turningOff = (newSpeed == 0);
  362.  
  363.   if (turningOff)
  364.     prevFanSpeed = radio.currentFanState.fanSpeed;  // remember to restore on next "on"
  365.  
  366.   radio.currentFanState.fanSpeed = newSpeed;
  367.   fanSpeedRequest = newSpeed;                       // prevent RF update next cycle
  368.  
  369.   if (turningOff)
  370.     updateFanState(0);
  371.   else
  372.   {
  373.     if (wasOff) updateFanState(1);                  // off -> on transition: send V_STATUS once
  374.     updateFanSpeed(newSpeed);
  375.   }
  376. }
  377.  
  378. // Process the freshly-polled fan state. First call seeds HA-side state;
  379. // subsequent calls push telemetry deltas and observed speed changes.
  380. static void processFanState()
  381. {
  382.   static bool firstRun = true;
  383.  
  384.   if (firstRun)
  385.   {
  386.     firstRun = false;
  387.     radio.currentFanState.fanSpeed = radio.newFanState.fanSpeed;
  388.     fanSpeedRequest = radio.currentFanState.fanSpeed;          // prevent RF update next cycle
  389.     updateFanState(radio.currentFanState.fanSpeed == 0 ? 0 : 1);
  390.     updateFanSpeed(radio.currentFanState.fanSpeed);
  391.     return;
  392.   }
  393.  
  394.   updateNewParams();
  395.  
  396.   if (radio.currentFanState.fanSpeed != radio.newFanState.fanSpeed)
  397.     processFanSpeedChange();
  398. }
  399.  
  400. static void pollFanState()
  401. {
  402.   static uint8_t consecFails = 0;
  403.   const uint8_t POLL_FAIL_THRESHOLD = 3;
  404.  
  405.   unsigned long now = millis();
  406.   if ((now - prevPollMs) <= FAN_INTERVAL) return;
  407.   prevPollMs = now;  // anchor; no catch-up burst if a previous poll was starved
  408.  
  409.   if (!radio.requestFanState())
  410.   {
  411.     if (++consecFails == POLL_FAIL_THRESHOLD) debugMsg(F("poll fail"));
  412.     return;
  413.   }
  414.   consecFails = 0;
  415.   ledFlash(1);
  416.  
  417.   processFanState();
  418. }
  419.  
  420. // Returns true if it consumed this tick (attempted a TX or gave up the retry budget).
  421. static bool handleDemandRequest()
  422. {
  423.   static uint8_t retryCount = 0;
  424.   static unsigned long lastAttemptMs = 0;
  425.   // 0xFF (out-of-band for a 0..100 pct) so first HA write of 0 still produces changed==true.
  426.   static uint8_t currentPct = 0xFF;
  427.  
  428.   if (fanDemandRequest < 0) return false;
  429.  
  430.   // 0..100 -> 0..0xFF (8-byte Orcon 31E0 form). Fan-side curve clips to ~44..89.5%.
  431.   uint8_t demandByte = (uint8_t)((uint16_t)fanDemandRequest * 255 / 100);
  432.   unsigned long now = millis();
  433.   bool changed = (currentPct != (uint8_t)fanDemandRequest);
  434.   bool stale = (now - lastAttemptMs) >= DEMAND_REBROADCAST_MS;
  435.  
  436.   if (!changed && !stale) return false;
  437.  
  438.   // Anchor before TX so a busy transmitData doesn't refire 'stale' next iter.
  439.   // Note: this is "last attempt", not "last successful TX".
  440.   lastAttemptMs = now;
  441.  
  442.   if (retryCount >= TX_RETRY_COUNT)
  443.   {
  444.     // Give up this session — sync state so we don't refire 'changed' every loop.
  445.     currentPct = (uint8_t)fanDemandRequest;
  446.     retryCount = 0;
  447.     debugMsg(F("demand forced"));
  448.     return true;
  449.   }
  450.  
  451.   if (radio.txDemand(demandByte))
  452.   {
  453.     ledFlash(1);
  454.     currentPct = (uint8_t)fanDemandRequest;
  455.     retryCount = 0;
  456.   }
  457.   else
  458.   {
  459.     debugMsg(F("demand fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
  460.     retryCount++;
  461.   }
  462.   return true;
  463. }
  464.  
  465. // Handle a pending comfort-temperature write from HA. Pattern matches handleBypassRequest:
  466. // count attempts (not failures) so a transmitData success on a coincident 31DA — which
  467. // doesn't update newFanState.comfortTemperature — still trips the force budget instead
  468. // of looping forever.
  469. static bool handleComfortTempRequest()
  470. {
  471.   static uint8_t retryCount = 0;
  472.  
  473.   if (fanComfortTempRequest < 0) return false;
  474.   uint16_t targetCenti = (uint16_t)fanComfortTempRequest;
  475.  
  476.   if (radio.currentFanState.comfortTemperature == targetCenti)
  477.   {
  478.     fanComfortTempRequest = -1;
  479.     retryCount = 0;
  480.     return false;
  481.   }
  482.  
  483.   if (retryCount >= TX_RETRY_COUNT)
  484.   {
  485.     radio.currentFanState.comfortTemperature = targetCenti;
  486.     send(msgComfortTEMP.set(toCelsius(targetCenti), 1));
  487.     send(msgComfortPCT.set((int)(targetCenti / 100)));
  488.     fanComfortTempRequest = -1;
  489.     retryCount = 0;
  490.     debugMsg(F("comfort set forced"));
  491.     return true;
  492.   }
  493.  
  494.   if (radio.setComfortTemperature(targetCenti))
  495.   {
  496.     ledFlash(1);
  497.     if (radio.currentFanState.comfortTemperature != radio.newFanState.comfortTemperature)
  498.     {
  499.       radio.currentFanState.comfortTemperature = radio.newFanState.comfortTemperature;
  500.       send(msgComfortTEMP.set(toCelsius(radio.currentFanState.comfortTemperature), 1));
  501.       send(msgComfortPCT.set((int)(radio.currentFanState.comfortTemperature / 100)));
  502.     }
  503.   }
  504.   else
  505.   {
  506.     debugMsg(F("comfort set fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
  507.   }
  508.   retryCount++;
  509.   return true;
  510. }
  511.  
  512. // Periodic poll of the Orcon "Comfort temperature" (2411 param 0x75 — the bypass target).
  513. // Fires once on boot (nextPollMs starts at 0) and then every COMFORT_TEMP_POLL_INTERVAL_MS.
  514. // Returns true if it consumed this tick.
  515. static bool handleComfortTempPoll()
  516. {
  517.   static uint8_t retryCount = 0;
  518.   static unsigned long nextPollMs = 0;
  519.  
  520.   unsigned long now = millis();
  521.   if ((long)(now - nextPollMs) < 0) return false;
  522.  
  523.   if (retryCount >= TX_RETRY_COUNT)
  524.   {
  525.     debugMsg(F("comfort forced"));
  526.     retryCount = 0;
  527.     nextPollMs = now + COMFORT_TEMP_POLL_INTERVAL_MS;
  528.     return true;
  529.   }
  530.  
  531.   if (radio.requestComfortTemperature())
  532.   {
  533.     ledFlash(1);
  534.     retryCount = 0;
  535.     nextPollMs = now + COMFORT_TEMP_POLL_INTERVAL_MS;
  536.  
  537.     // Publish inline. processFanState() skips updateNewParams() on its first run, so
  538.     // routing the diff through there would delay the post-boot value by an extra 5s.
  539.     if (radio.currentFanState.comfortTemperature != radio.newFanState.comfortTemperature)
  540.     {
  541.       send(msgComfortTEMP.set(toCelsius(radio.newFanState.comfortTemperature), 1));
  542.       send(msgComfortPCT.set((int)(radio.newFanState.comfortTemperature / 100)));
  543.       radio.currentFanState.comfortTemperature = radio.newFanState.comfortTemperature;
  544.     }
  545.   }
  546.   else
  547.   {
  548.     debugMsg(F("comfort fail %u/%u"), (unsigned)(retryCount + 1), (unsigned)TX_RETRY_COUNT);
  549.     retryCount++;
  550.     nextPollMs = now + COMFORT_TEMP_RETRY_INTERVAL_MS;
  551.   }
  552.   return true;
  553. }
  554.  
  555. static void handleJustBooted()
  556. {
  557.   ledFlash(1);
  558.  
  559.   send(msgSourceAddress.set(0));
  560.   send(msgTargetAddress.set(0));
  561.   send(msgCloneSwitch.set(0));
  562.   send(msgFANspeed.set(radio.currentFanState.fanSpeed));
  563.   send(msgFANstate.set(radio.currentFanState.fanSpeed == 0 ? 0 : 1));
  564.   send(msgBypassMODE_VAL.set(HA_BYPASS_UNKNOWN));
  565.   send(msgBypassMODE_STATE.set(1)); // On
  566.   send(msgDEMAND_PCT.set(0));
  567.   send(msgDEMAND_STATE.set(0));
  568.   send(msgComfortTEMP.set(toCelsius(radio.currentFanState.comfortTemperature), 1));
  569.   send(msgComfortSTATE.set(1)); // S_DIMMER stays "on"; the % value carries the °C setpoint
  570.   send(msgComfortPCT.set((int)(radio.currentFanState.comfortTemperature / 100)));
  571.  
  572.   if ((radio.readReg(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) & 0x1f) != 1)
  573.   {
  574.     wait(1000);
  575.     return;
  576.   }
  577.  
  578.   // Radio is alive — check EEPROM for a paired address.
  579.   bool emptyEeprom = true;
  580.   for (int i = 0; i < 6; i++)
  581.   {
  582.     radio.newFanState.address[i] = loadState(i);
  583.     if (radio.newFanState.address[i] != 0xFF) emptyEeprom = false;
  584.   }
  585.  
  586.   if (emptyEeprom)
  587.   {
  588.     dongleState = PAIRING_FAIL;
  589.     return;
  590.   }
  591.  
  592.   sendNewSourceAddressToGateway();
  593.   sendNewTargetAddressToGateway();
  594.   dongleState = NORMAL_MODE;
  595.   debugMsg(F("boot ok v" SV));
  596. }
  597.  
  598. static void handlePairingMode()
  599. {
  600.   digitalWrite(ONBOARD_LED, LOW);  // RADIO LED ON
  601.  
  602.   radio.setRxMode();
  603.   if (radio.cloneMode())
  604.   {
  605.     for (int i = 0; i < 6; i++)
  606.       saveState(i, radio.newFanState.address[i]);
  607.  
  608.     sendNewSourceAddressToGateway();
  609.     sendNewTargetAddressToGateway();
  610.  
  611.     prevPollMs = millis();  // prevent immediate burst on entering NORMAL_MODE
  612.     dongleState = NORMAL_MODE;
  613.     debugMsg(F("paired v" SV));
  614.   }
  615.   else
  616.     dongleState = PAIRING_FAIL;
  617.  
  618.   digitalWrite(ONBOARD_LED, HIGH); // RADIO LED OFF
  619.   cloneModeEnded();
  620. }
  621.  
  622. //******************************************************************************************//
  623. //                                                                                          //
  624. //                        Init                                                              //
  625. //                                                                                          //
  626. //******************************************************************************************//
  627.  
  628. void before()
  629. {
  630.   radio.init();
  631. }
  632.  
  633. void setup()
  634. {
  635.   wdt_disable();
  636.   pinMode(ONBOARD_LED, OUTPUT);
  637.   digitalWrite(ONBOARD_LED, HIGH);
  638.   Serial1.begin(38400); // used for transmitting data to FAN
  639.   while (!Serial1);
  640.   radio.currentFanState.fanSpeed = 1;
  641.   radio.currentFanState.bypassMode = BYPASS_UNKNOWN; // matches fanBypassModeRequest so we don't TX before a real value arrives
  642.   wdt_enable(WDTO_8S);
  643. }
  644.  
  645. void presentation()
  646. {
  647.   sendSketchInfo(F(SN), F(SV));
  648.   present(CHILD_ID_FAN, S_DIMMER, F("FAN speed"));
  649.   present(CHILD_ID_CLONE, S_BINARY, F("Clone switch"));
  650.   present(CHILD_ID_TARGET_ADDRESS, S_CUSTOM, F("Target address"));
  651.   present(CHILD_ID_SOURCE_ADDRESS, S_CUSTOM, F("Source address"));
  652.  
  653.   present(CHILD_ID_INDOOR_HUM, S_HUM, F("Indoor humidity (%)"));
  654.   present(CHILD_ID_OUTDOOR_HUM, S_HUM, F("Outdoor humidity (%)"));
  655.   present(CHILD_ID_EXHAUST_TEMP, S_TEMP, F("Exhaust temperature (ºC)"));
  656.   present(CHILD_ID_SUPPLY_TEMP, S_TEMP, F("Supply temperature (ºC)"));
  657.  
  658.   present(CHILD_ID_BYPASS_POS, S_CUSTOM, F("Bypass position (%)"));
  659.   present(CHILD_ID_EXHAUST_FANSPEED, S_CUSTOM, F("Exhaust FAN (%)"));
  660.   present(CHILD_ID_SUPPLY_FANSPEED, S_CUSTOM, F("Supply FAN (%)"));
  661.  
  662.   present(CHILD_ID_SUPPLY_FLOW, S_CUSTOM, F("Supply flow (m3/h)"));
  663.   present(CHILD_ID_EXHAUST_FLOW, S_CUSTOM, F("Exhaust flow (m3/h)"));
  664.  
  665.   present(CHILD_ID_INDOOR_TEMP, S_TEMP, F("Indoor temperature (ºC)"));
  666.   present(CHILD_ID_OUTDOOR_TEMP, S_TEMP, F("Outdoor temperature (ºC)"));
  667.  
  668.   present(CHILD_ID_BYPASS_MODE, S_DIMMER, F("Bypass mode (1 , 2, 3)"));
  669.  
  670.   present(CHILD_ID_DEMAND, S_DIMMER, F("Fan demand"));
  671.  
  672.   present(CHILD_ID_COMFORT_TEMP, S_TEMP, F("Comfort Temperature"));
  673.   present(CHILD_ID_COMFORT_SETPOINT, S_DIMMER, F("Comfort Setpoint"));
  674.  
  675.   present(CHILD_ID_DEBUG, S_INFO, F("Debug status"));
  676. }
  677.  
  678. //******************************************************************************************//
  679. //                                                                                          //
  680. //                        MAIN                                                              //
  681. //                                                                                          //
  682. //******************************************************************************************//
  683.  
  684. void loop()
  685. {
  686.   switch (dongleState)
  687.   {
  688.     case JUST_BOOTED:
  689.       handleJustBooted();
  690.       break;
  691.  
  692.     case RF15_PAIRINGSMODE:
  693.       handlePairingMode();
  694.       break;
  695.  
  696.     case PAIRING_FAIL:
  697.       ledFlash(2);
  698.       wait(750);
  699.       break;
  700.  
  701.     case NORMAL_MODE:
  702.       // At most one command per tick (shared radio, priority order).
  703.       // If no request was pending, fall back to polling fan state.
  704.       if (handleFanSpeedRequest()) break;
  705.       if (handleBypassRequest()) break;
  706.       if (handleDemandRequest()) break;
  707.       if (handleComfortTempRequest()) break;
  708.       if (handleComfortTempPoll()) break;
  709.       pollFanState();
  710.       break;
  711.   }
  712.  
  713.   wdt_reset();
  714. }
  715.  
  716. //******************************************************************************************//
  717. //                                                                                          //
  718. //                        Receive from MySensors                                            //
  719. //                                                                                          //
  720. //******************************************************************************************//
  721.  
  722. void receive(const MyMessage &message)
  723. {
  724.   if(message.isAck())
  725.   {
  726.     return;
  727.   }
  728.  
  729.   if( (message.getType() == V_STATUS) && (message.getSensor() == CHILD_ID_CLONE) )
  730.   {
  731.     if(message.getBool())
  732.     {
  733.       dongleState = RF15_PAIRINGSMODE;
  734.       cloneModeStarted();
  735.     }
  736.   }
  737.  
  738.   if(message.getSensor() == CHILD_ID_FAN)
  739.   {
  740.     if(message.getType() == V_STATUS)
  741.     {
  742.       if(!message.getBool())
  743.       {
  744.         // Remember speed so a bare V_STATUS=on (no brightness) can restore it.
  745.         prevFanSpeed = radio.currentFanState.fanSpeed;
  746.         fanSpeedRequest = 0;
  747.         updateFanState(0);
  748.       }
  749.       else
  750.       {
  751.         fanSpeedRequest = prevFanSpeed;
  752.         updateFanState(1);
  753.       }
  754.     }
  755.  
  756.     if (message.getType() == V_PERCENTAGE)
  757.     {
  758.       fanSpeedRequest = constrain(message.getInt(), 0, 4);
  759.       updateFanSpeed(fanSpeedRequest);
  760.     }
  761.   }
  762.  
  763.   if(message.getSensor() == CHILD_ID_BYPASS_MODE)
  764.   {
  765.     if (message.getType() == V_PERCENTAGE)
  766.     {
  767.       int16_t haBypassValue = constrain(message.getInt(), 1, 3);
  768.       updateBypassMode(haBypassValue);
  769.  
  770.       switch (haBypassValue)
  771.       {
  772.         case HA_BYPASS_AUTO:  fanBypassModeRequest = BYPASS_AUTO;  break;
  773.         case HA_BYPASS_OPEN:  fanBypassModeRequest = BYPASS_OPEN;  break;
  774.         case HA_BYPASS_CLOSE: fanBypassModeRequest = BYPASS_CLOSE; break;
  775.         default: break;
  776.       }
  777.     }
  778.   }
  779.  
  780.   if (message.getSensor() == CHILD_ID_DEMAND)
  781.   {
  782.     if (message.getType() == V_PERCENTAGE)
  783.     {
  784.       fanDemandRequest = constrain(message.getInt(), 0, 100);
  785.     }
  786.     else if (message.getType() == V_STATUS)
  787.     {
  788.       // HA "light off" -> demand 0. Bare "on" (no brightness) is ignored:
  789.       // there's no meaningful previous-value to restore for a continuous setpoint.
  790.       if (!message.getBool()) fanDemandRequest = 0;
  791.     }
  792.   }
  793.  
  794.   if (message.getSensor() == CHILD_ID_COMFORT_SETPOINT)
  795.   {
  796.     if (message.getType() == V_PERCENTAGE)
  797.     {
  798.       // The slider's % value IS the °C setpoint (0..COMFORT_SETPOINT_MAX_C).
  799.       int pct = constrain(message.getInt(), 0, COMFORT_SETPOINT_MAX_C);
  800.       fanComfortTempRequest = (int16_t)(pct * 100);
  801.       send(msgComfortPCT.set(pct));
  802.     }
  803.     else if (message.getType() == V_STATUS)
  804.     {
  805.       // S_DIMMER also accepts on/off. Treat both as no-op: there's no "off"
  806.       // for a setpoint, and bare "on" carries no value to apply.
  807.       send(msgComfortSTATE.set(1));
  808.     }
  809.   }
  810.  
  811. }
  812.  
Advertisement
Add Comment
Please, Sign In to add comment