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.

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