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