Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

572 lines
16 KiB

5 years ago
  1. /*
  2. HOME ASSISTANT MODULE
  3. Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #if HOMEASSISTANT_SUPPORT
  6. #include <Ticker.h>
  7. #include <Schedule.h>
  8. #include "homeassistant.h"
  9. #include "mqtt.h"
  10. #include "relay.h"
  11. #include "rpc.h"
  12. #include "utils.h"
  13. #include "ws.h"
  14. bool _ha_enabled = false;
  15. bool _ha_send_flag = false;
  16. // -----------------------------------------------------------------------------
  17. // UTILS
  18. // -----------------------------------------------------------------------------
  19. // per yaml 1.1 spec, following scalars are converted to bool. we want the string, so quoting the output
  20. // y|Y|yes|Yes|YES|n|N|no|No|NO |true|True|TRUE|false|False|FALSE |on|On|ON|off|Off|OFF
  21. String _haFixPayload(const String& value) {
  22. if (value.equalsIgnoreCase("y")
  23. || value.equalsIgnoreCase("n")
  24. || value.equalsIgnoreCase("yes")
  25. || value.equalsIgnoreCase("no")
  26. || value.equalsIgnoreCase("true")
  27. || value.equalsIgnoreCase("false")
  28. || value.equalsIgnoreCase("on")
  29. || value.equalsIgnoreCase("off")
  30. ) {
  31. String temp;
  32. temp.reserve(value.length() + 2);
  33. temp = "\"";
  34. temp += value;
  35. temp += "\"";
  36. return temp;
  37. }
  38. return value;
  39. }
  40. String& _haFixName(String& name) {
  41. for (unsigned char i=0; i<name.length(); i++) {
  42. if (!isalnum(name.charAt(i))) name.setCharAt(i, '_');
  43. }
  44. return name;
  45. }
  46. #if (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE) || (defined(ITEAD_SLAMPHER))
  47. const String switchType("light");
  48. #else
  49. const String switchType("switch");
  50. #endif
  51. // -----------------------------------------------------------------------------
  52. // Shared context object to store entity and entity registry data
  53. // -----------------------------------------------------------------------------
  54. struct ha_config_t {
  55. static const size_t DEFAULT_BUFFER_SIZE = 2048;
  56. ha_config_t(size_t size) :
  57. jsonBuffer(size),
  58. deviceConfig(jsonBuffer.createObject()),
  59. root(jsonBuffer.createObject()),
  60. identifier(getIdentifier()),
  61. name(getSetting("desc", getSetting("hostname"))),
  62. version(String(APP_NAME " " APP_VERSION " (") + getCoreVersion() + ")")
  63. {
  64. deviceConfig.createNestedArray("identifiers").add(identifier.c_str());
  65. deviceConfig["name"] = name.c_str();
  66. deviceConfig["sw_version"] = version.c_str();
  67. deviceConfig["manufacturer"] = getDevice().c_str();
  68. deviceConfig["model"] = getManufacturer().c_str();
  69. }
  70. ha_config_t() : ha_config_t(DEFAULT_BUFFER_SIZE) {}
  71. size_t size() { return jsonBuffer.size(); }
  72. DynamicJsonBuffer jsonBuffer;
  73. JsonObject& deviceConfig;
  74. JsonObject& root;
  75. const String identifier;
  76. const String name;
  77. const String version;
  78. };
  79. // -----------------------------------------------------------------------------
  80. // MQTT discovery
  81. // -----------------------------------------------------------------------------
  82. struct ha_discovery_t {
  83. constexpr static const unsigned long SEND_TIMEOUT = 1000;
  84. constexpr static const unsigned char SEND_RETRY = 5;
  85. ha_discovery_t() :
  86. _retry(SEND_RETRY)
  87. {
  88. #if SENSOR_SUPPORT
  89. _messages.reserve(magnitudeCount() + relayCount());
  90. #else
  91. _messages.reserve(relayCount());
  92. #endif
  93. }
  94. ~ha_discovery_t() {
  95. DEBUG_MSG_P(PSTR("[HA] Discovery %s\n"), empty() ? "OK" : "FAILED");
  96. }
  97. // TODO: is this expected behaviour?
  98. void add(String& topic, String& message) {
  99. _messages.emplace_back(std::move(topic), std::move(message));
  100. }
  101. // We don't particulary care about the order since names have indexes?
  102. // If we ever do, use iterators to reference elems and pop the String contents instead
  103. mqtt_msg_t& next() {
  104. return _messages.back();
  105. }
  106. void pop() {
  107. _messages.pop_back();
  108. }
  109. const bool empty() const {
  110. return !_messages.size();
  111. }
  112. bool retry() {
  113. if (!_retry) return false;
  114. return --_retry;
  115. }
  116. void prepareSwitches(ha_config_t& config);
  117. #if SENSOR_SUPPORT
  118. void prepareMagnitudes(ha_config_t& config);
  119. #endif
  120. Ticker timer;
  121. std::vector<mqtt_msg_t> _messages;
  122. unsigned char _retry;
  123. };
  124. std::unique_ptr<ha_discovery_t> _ha_discovery = nullptr;
  125. void _haSendDiscovery() {
  126. if (!_ha_discovery) return;
  127. const bool connected = mqttConnected();
  128. const bool retry = _ha_discovery->retry();
  129. const bool empty = _ha_discovery->empty();
  130. if (!connected || !retry || empty) {
  131. _ha_discovery = nullptr;
  132. return;
  133. }
  134. const unsigned long ts = millis();
  135. do {
  136. if (_ha_discovery->empty()) break;
  137. auto& message = _ha_discovery->next();
  138. if (!mqttSendRaw(message.first.c_str(), message.second.c_str())) {
  139. break;
  140. }
  141. _ha_discovery->pop();
  142. // XXX: should not reach this timeout, most common case is the break above
  143. } while (millis() - ts < ha_discovery_t::SEND_TIMEOUT);
  144. mqttSendStatus();
  145. if (_ha_discovery->empty()) {
  146. _ha_discovery = nullptr;
  147. } else {
  148. // 2.3.0: Ticker callback arguments are not preserved and once_ms_scheduled is missing
  149. // We need to use global discovery object to reschedule it
  150. // Otherwise, this would've been shared_ptr from _haSend
  151. _ha_discovery->timer.once_ms(ha_discovery_t::SEND_TIMEOUT, []() {
  152. schedule_function(_haSendDiscovery);
  153. });
  154. }
  155. }
  156. // -----------------------------------------------------------------------------
  157. // SENSORS
  158. // -----------------------------------------------------------------------------
  159. #if SENSOR_SUPPORT
  160. void _haSendMagnitude(unsigned char index, JsonObject& config) {
  161. config["name"] = _haFixName(getSetting("hostname") + String(" ") + magnitudeTopic(magnitudeType(index)));
  162. config["state_topic"] = mqttTopic(magnitudeTopicIndex(index).c_str(), false);
  163. config["unit_of_measurement"] = magnitudeUnits(index);
  164. }
  165. void ha_discovery_t::prepareMagnitudes(ha_config_t& config) {
  166. // Note: because none of the keys are erased, use a separate object to avoid accidentally sending switch data
  167. JsonObject& root = config.jsonBuffer.createObject();
  168. for (unsigned char i=0; i<magnitudeCount(); i++) {
  169. String topic = getSetting("haPrefix", HOMEASSISTANT_PREFIX) +
  170. "/sensor/" +
  171. getSetting("hostname") + "_" + String(i) +
  172. "/config";
  173. String message;
  174. if (_ha_enabled) {
  175. _haSendMagnitude(i, root);
  176. root["uniq_id"] = getIdentifier() + "_" + magnitudeTopic(magnitudeType(i)) + "_" + String(i);
  177. root["device"] = config.deviceConfig;
  178. message.reserve(root.measureLength());
  179. root.printTo(message);
  180. }
  181. add(topic, message);
  182. }
  183. }
  184. #endif // SENSOR_SUPPORT
  185. // -----------------------------------------------------------------------------
  186. // SWITCHES & LIGHTS
  187. // -----------------------------------------------------------------------------
  188. void _haSendSwitch(unsigned char i, JsonObject& config) {
  189. String name = getSetting("hostname");
  190. if (relayCount() > 1) {
  191. name += String("_") + String(i);
  192. }
  193. config.set("name", _haFixName(name));
  194. if (relayCount()) {
  195. config["state_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, false);
  196. config["command_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, true);
  197. config["payload_on"] = relayPayload(PayloadStatus::On);
  198. config["payload_off"] = relayPayload(PayloadStatus::Off);
  199. config["availability_topic"] = mqttTopic(MQTT_TOPIC_STATUS, false);
  200. config["payload_available"] = mqttPayloadStatus(true);
  201. config["payload_not_available"] = mqttPayloadStatus(false);
  202. }
  203. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  204. if (i == 0) {
  205. config["brightness_state_topic"] = mqttTopic(MQTT_TOPIC_BRIGHTNESS, false);
  206. config["brightness_command_topic"] = mqttTopic(MQTT_TOPIC_BRIGHTNESS, true);
  207. if (lightHasColor()) {
  208. config["rgb_state_topic"] = mqttTopic(MQTT_TOPIC_COLOR_RGB, false);
  209. config["rgb_command_topic"] = mqttTopic(MQTT_TOPIC_COLOR_RGB, true);
  210. }
  211. if (lightHasColor() || lightUseCCT()) {
  212. config["color_temp_command_topic"] = mqttTopic(MQTT_TOPIC_MIRED, true);
  213. config["color_temp_state_topic"] = mqttTopic(MQTT_TOPIC_MIRED, false);
  214. }
  215. if (lightChannels() > 3) {
  216. config["white_value_state_topic"] = mqttTopic(MQTT_TOPIC_CHANNEL, 3, false);
  217. config["white_value_command_topic"] = mqttTopic(MQTT_TOPIC_CHANNEL, 3, true);
  218. }
  219. }
  220. #endif // LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  221. }
  222. void ha_discovery_t::prepareSwitches(ha_config_t& config) {
  223. // Note: because none of the keys are erased, use a separate object to avoid accidentally sending magnitude data
  224. JsonObject& root = config.jsonBuffer.createObject();
  225. for (unsigned char i=0; i<relayCount(); i++) {
  226. String topic = getSetting("haPrefix", HOMEASSISTANT_PREFIX) +
  227. "/" + switchType +
  228. "/" + getSetting("hostname") + "_" + String(i) +
  229. "/config";
  230. String message;
  231. if (_ha_enabled) {
  232. _haSendSwitch(i, root);
  233. root["uniq_id"] = getIdentifier() + "_" + switchType + "_" + String(i);
  234. root["device"] = config.deviceConfig;
  235. message.reserve(root.measureLength());
  236. root.printTo(message);
  237. }
  238. add(topic, message);
  239. }
  240. }
  241. // -----------------------------------------------------------------------------
  242. constexpr const size_t HA_YAML_BUFFER_SIZE = 1024;
  243. void _haSwitchYaml(unsigned char index, JsonObject& root) {
  244. String output;
  245. output.reserve(HA_YAML_BUFFER_SIZE);
  246. JsonObject& config = root.createNestedObject("config");
  247. config["platform"] = "mqtt";
  248. _haSendSwitch(index, config);
  249. if (index == 0) output += "\n\n" + switchType + ":";
  250. output += "\n";
  251. bool first = true;
  252. for (auto kv : config) {
  253. if (first) {
  254. output += " - ";
  255. first = false;
  256. } else {
  257. output += " ";
  258. }
  259. output += kv.key;
  260. output += ": ";
  261. if (strncmp(kv.key, "payload_", strlen("payload_")) == 0) {
  262. output += _haFixPayload(kv.value.as<String>());
  263. } else {
  264. output += kv.value.as<String>();
  265. }
  266. output += "\n";
  267. }
  268. output += " ";
  269. root.remove("config");
  270. root["haConfig"] = output;
  271. }
  272. #if SENSOR_SUPPORT
  273. void _haSensorYaml(unsigned char index, JsonObject& root) {
  274. String output;
  275. output.reserve(HA_YAML_BUFFER_SIZE);
  276. JsonObject& config = root.createNestedObject("config");
  277. config["platform"] = "mqtt";
  278. _haSendMagnitude(index, config);
  279. if (index == 0) output += "\n\nsensor:";
  280. output += "\n";
  281. bool first = true;
  282. for (auto kv : config) {
  283. if (first) {
  284. output += " - ";
  285. first = false;
  286. } else {
  287. output += " ";
  288. }
  289. String value = kv.value.as<String>();
  290. value.replace("%", "'%'");
  291. output += kv.key;
  292. output += ": ";
  293. output += value;
  294. output += "\n";
  295. }
  296. output += " ";
  297. root.remove("config");
  298. root["haConfig"] = output;
  299. }
  300. #endif // SENSOR_SUPPORT
  301. void _haGetDeviceConfig(JsonObject& config) {
  302. config.createNestedArray("identifiers").add(getIdentifier());
  303. config["name"] = getSetting("desc", getSetting("hostname"));
  304. config["manufacturer"] = getManufacturer().c_str();
  305. config["model"] = getDevice().c_str();
  306. config["sw_version"] = String(APP_NAME) + " " + APP_VERSION + " (" + getCoreVersion() + ")";
  307. }
  308. void _haSend() {
  309. // Pending message to send?
  310. if (!_ha_send_flag) return;
  311. // Are we connected?
  312. if (!mqttConnected()) return;
  313. // Are we still trying to send discovery messages?
  314. if (_ha_discovery) return;
  315. DEBUG_MSG_P(PSTR("[HA] Preparing MQTT discovery message(s)...\n"));
  316. // Get common device config / context object
  317. ha_config_t config;
  318. // We expect only one instance, create now
  319. _ha_discovery = std::make_unique<ha_discovery_t>();
  320. // Prepare all of the messages and send them in the scheduled function later
  321. _ha_discovery->prepareSwitches(config);
  322. #if SENSOR_SUPPORT
  323. _ha_discovery->prepareMagnitudes(config);
  324. #endif
  325. _ha_send_flag = false;
  326. schedule_function(_haSendDiscovery);
  327. }
  328. void _haConfigure() {
  329. const bool enabled = getSetting("haEnabled", 1 == HOMEASSISTANT_ENABLED);
  330. _ha_send_flag = (enabled != _ha_enabled);
  331. _ha_enabled = enabled;
  332. // https://github.com/xoseperez/espurna/issues/1273
  333. // https://gitter.im/tinkerman-cat/espurna?at=5df8ad4655d9392300268a8c
  334. // TODO: ensure that this is called before _lightConfigure()
  335. // in case useCSS value is ever cached by the lights module
  336. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  337. if (enabled) {
  338. if (getSetting("useCSS", 1 == LIGHT_USE_CSS)) {
  339. setSetting("useCSS", 0);
  340. }
  341. }
  342. #endif
  343. _haSend();
  344. }
  345. #if WEB_SUPPORT
  346. bool _haWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  347. return (strncmp(key, "ha", 2) == 0);
  348. }
  349. void _haWebSocketOnVisible(JsonObject& root) {
  350. root["haVisible"] = 1;
  351. }
  352. void _haWebSocketOnConnected(JsonObject& root) {
  353. root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX);
  354. root["haEnabled"] = getSetting("haEnabled", 1 == HOMEASSISTANT_ENABLED);
  355. }
  356. void _haWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
  357. if (strcmp(action, "haconfig") == 0) {
  358. ws_on_send_callback_list_t callbacks;
  359. #if SENSOR_SUPPORT
  360. callbacks.reserve(magnitudeCount() + relayCount());
  361. #else
  362. callbacks.reserve(relayCount());
  363. #endif // SENSOR_SUPPORT
  364. {
  365. for (unsigned char idx=0; idx<relayCount(); ++idx) {
  366. callbacks.push_back([idx](JsonObject& root) {
  367. _haSwitchYaml(idx, root);
  368. });
  369. }
  370. }
  371. #if SENSOR_SUPPORT
  372. {
  373. for (unsigned char idx=0; idx<magnitudeCount(); ++idx) {
  374. callbacks.push_back([idx](JsonObject& root) {
  375. _haSensorYaml(idx, root);
  376. });
  377. }
  378. }
  379. #endif // SENSOR_SUPPORT
  380. if (callbacks.size()) wsPostSequence(client_id, std::move(callbacks));
  381. }
  382. }
  383. #endif // WEB_SUPPORT
  384. #if TERMINAL_SUPPORT
  385. void _haInitCommands() {
  386. terminalRegisterCommand(F("HA.CONFIG"), [](Embedis* e) {
  387. for (unsigned char idx=0; idx<relayCount(); ++idx) {
  388. DynamicJsonBuffer jsonBuffer(1024);
  389. JsonObject& root = jsonBuffer.createObject();
  390. _haSwitchYaml(idx, root);
  391. DEBUG_MSG(root["haConfig"].as<String>().c_str());
  392. }
  393. #if SENSOR_SUPPORT
  394. for (unsigned char idx=0; idx<magnitudeCount(); ++idx) {
  395. DynamicJsonBuffer jsonBuffer(1024);
  396. JsonObject& root = jsonBuffer.createObject();
  397. _haSensorYaml(idx, root);
  398. DEBUG_MSG(root["haConfig"].as<String>().c_str());
  399. }
  400. #endif // SENSOR_SUPPORT
  401. DEBUG_MSG("\n");
  402. terminalOK();
  403. });
  404. terminalRegisterCommand(F("HA.SEND"), [](Embedis* e) {
  405. setSetting("haEnabled", "1");
  406. _haConfigure();
  407. #if WEB_SUPPORT
  408. wsPost(_haWebSocketOnConnected);
  409. #endif
  410. terminalOK();
  411. });
  412. terminalRegisterCommand(F("HA.CLEAR"), [](Embedis* e) {
  413. setSetting("haEnabled", "0");
  414. _haConfigure();
  415. #if WEB_SUPPORT
  416. wsPost(_haWebSocketOnConnected);
  417. #endif
  418. terminalOK();
  419. });
  420. }
  421. #endif
  422. // -----------------------------------------------------------------------------
  423. void haSetup() {
  424. _haConfigure();
  425. #if WEB_SUPPORT
  426. wsRegister()
  427. .onVisible(_haWebSocketOnVisible)
  428. .onConnected(_haWebSocketOnConnected)
  429. .onAction(_haWebSocketOnAction)
  430. .onKeyCheck(_haWebSocketOnKeyCheck);
  431. #endif
  432. #if TERMINAL_SUPPORT
  433. _haInitCommands();
  434. #endif
  435. // On MQTT connect check if we have something to send
  436. mqttRegister([](unsigned int type, const char * topic, const char * payload) {
  437. if (type == MQTT_CONNECT_EVENT) schedule_function(_haSend);
  438. if (type == MQTT_DISCONNECT_EVENT) _ha_send_flag = _ha_enabled;
  439. });
  440. // Main callbacks
  441. espurnaRegisterReload(_haConfigure);
  442. }
  443. #endif // HOMEASSISTANT_SUPPORT