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.

439 lines
12 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 <ArduinoJson.h>
  7. bool _haEnabled = false;
  8. bool _haSendFlag = false;
  9. // -----------------------------------------------------------------------------
  10. // UTILS
  11. // -----------------------------------------------------------------------------
  12. // per yaml 1.1 spec, following scalars are converted to bool. we want the string, so quoting the output
  13. // y|Y|yes|Yes|YES|n|N|no|No|NO |true|True|TRUE|false|False|FALSE |on|On|ON|off|Off|OFF
  14. String _haFixPayload(const String& value) {
  15. if (value.equalsIgnoreCase("y")
  16. || value.equalsIgnoreCase("n")
  17. || value.equalsIgnoreCase("yes")
  18. || value.equalsIgnoreCase("no")
  19. || value.equalsIgnoreCase("true")
  20. || value.equalsIgnoreCase("false")
  21. || value.equalsIgnoreCase("on")
  22. || value.equalsIgnoreCase("off")
  23. ) {
  24. String temp;
  25. temp.reserve(value.length() + 2);
  26. temp = "\"";
  27. temp += value;
  28. temp += "\"";
  29. return temp;
  30. }
  31. return value;
  32. }
  33. String& _haFixName(String& name) {
  34. for (unsigned char i=0; i<name.length(); i++) {
  35. if (!isalnum(name.charAt(i))) name.setCharAt(i, '_');
  36. }
  37. return name;
  38. }
  39. #if (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE) || (defined(ITEAD_SLAMPHER))
  40. const String switchType("light");
  41. #else
  42. const String switchType("switch");
  43. #endif
  44. struct ha_config_t {
  45. static const size_t DEFAULT_BUFFER_SIZE = 2048;
  46. ha_config_t(size_t size) :
  47. jsonBuffer(size),
  48. deviceConfig(jsonBuffer.createObject()),
  49. root(jsonBuffer.createObject()),
  50. identifier(getIdentifier()),
  51. name(getSetting("desc", getSetting("hostname"))),
  52. version(String(APP_NAME " " APP_VERSION " (") + getCoreVersion() + ")")
  53. {
  54. deviceConfig.createNestedArray("identifiers").add(identifier.c_str());
  55. deviceConfig["name"] = name.c_str();
  56. deviceConfig["sw_version"] = version.c_str();
  57. deviceConfig["manufacturer"] = MANUFACTURER;
  58. deviceConfig["model"] = DEVICE;
  59. }
  60. ha_config_t() : ha_config_t(DEFAULT_BUFFER_SIZE) {}
  61. size_t size() { return jsonBuffer.size(); }
  62. DynamicJsonBuffer jsonBuffer;
  63. JsonObject& deviceConfig;
  64. JsonObject& root;
  65. const String identifier;
  66. const String name;
  67. const String version;
  68. };
  69. // -----------------------------------------------------------------------------
  70. // SENSORS
  71. // -----------------------------------------------------------------------------
  72. #if SENSOR_SUPPORT
  73. void _haSendMagnitude(unsigned char i, JsonObject& config) {
  74. unsigned char type = magnitudeType(i);
  75. config["name"] = _haFixName(getSetting("hostname") + String(" ") + magnitudeTopic(type));
  76. config["state_topic"] = mqttTopic(magnitudeTopicIndex(i).c_str(), false);
  77. config["unit_of_measurement"] = magnitudeUnits(type);
  78. }
  79. void _haSendMagnitudes(ha_config_t& config) {
  80. for (unsigned char i=0; i<magnitudeCount(); i++) {
  81. String topic = getSetting("haPrefix", HOMEASSISTANT_PREFIX) +
  82. "/sensor/" +
  83. getSetting("hostname") + "_" + String(i) +
  84. "/config";
  85. String output;
  86. if (_haEnabled) {
  87. _haSendMagnitude(i, config.root);
  88. config.root["uniq_id"] = getIdentifier() + "_" + magnitudeTopic(magnitudeType(i)) + "_" + String(i);
  89. config.root["device"] = config.deviceConfig;
  90. output.reserve(config.root.measureLength());
  91. config.root.printTo(output);
  92. }
  93. mqttSendRaw(topic.c_str(), output.c_str());
  94. }
  95. mqttSendStatus();
  96. }
  97. #endif // SENSOR_SUPPORT
  98. // -----------------------------------------------------------------------------
  99. // SWITCHES & LIGHTS
  100. // -----------------------------------------------------------------------------
  101. void _haSendSwitch(unsigned char i, JsonObject& config) {
  102. String name = getSetting("hostname");
  103. if (relayCount() > 1) {
  104. name += String("_") + String(i);
  105. }
  106. config.set("name", _haFixName(name));
  107. if (relayCount()) {
  108. config["state_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, false);
  109. config["command_topic"] = mqttTopic(MQTT_TOPIC_RELAY, i, true);
  110. config["payload_on"] = relayPayload(RelayStatus::ON);
  111. config["payload_off"] = relayPayload(RelayStatus::OFF);
  112. config["availability_topic"] = mqttTopic(MQTT_TOPIC_STATUS, false);
  113. config["payload_available"] = mqttPayloadStatus(true);
  114. config["payload_not_available"] = mqttPayloadStatus(false);
  115. }
  116. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  117. if (i == 0) {
  118. config["brightness_state_topic"] = mqttTopic(MQTT_TOPIC_BRIGHTNESS, false);
  119. config["brightness_command_topic"] = mqttTopic(MQTT_TOPIC_BRIGHTNESS, true);
  120. if (lightHasColor()) {
  121. config["rgb_state_topic"] = mqttTopic(MQTT_TOPIC_COLOR_RGB, false);
  122. config["rgb_command_topic"] = mqttTopic(MQTT_TOPIC_COLOR_RGB, true);
  123. }
  124. if (lightUseCCT()) {
  125. config["color_temp_command_topic"] = mqttTopic(MQTT_TOPIC_MIRED, true);
  126. config["color_temp_state_topic"] = mqttTopic(MQTT_TOPIC_MIRED, false);
  127. }
  128. if (lightChannels() > 3) {
  129. config["white_value_state_topic"] = mqttTopic(MQTT_TOPIC_CHANNEL, 3, false);
  130. config["white_value_command_topic"] = mqttTopic(MQTT_TOPIC_CHANNEL, 3, true);
  131. }
  132. }
  133. #endif // LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  134. }
  135. void _haSendSwitches(ha_config_t& config) {
  136. for (unsigned char i=0; i<relayCount(); i++) {
  137. String topic = getSetting("haPrefix", HOMEASSISTANT_PREFIX) +
  138. "/" + switchType +
  139. "/" + getSetting("hostname") + "_" + String(i) +
  140. "/config";
  141. String output;
  142. if (_haEnabled) {
  143. _haSendSwitch(i, config.root);
  144. config.root["uniq_id"] = getIdentifier() + "_" + switchType + "_" + String(i);
  145. config.root["device"] = config.deviceConfig;
  146. output.reserve(config.root.measureLength());
  147. config.root.printTo(output);
  148. }
  149. mqttSendRaw(topic.c_str(), output.c_str());
  150. }
  151. mqttSendStatus();
  152. }
  153. // -----------------------------------------------------------------------------
  154. constexpr const size_t HA_YAML_BUFFER_SIZE = 1024;
  155. void _haSwitchYaml(unsigned char index, JsonObject& root) {
  156. String output;
  157. output.reserve(HA_YAML_BUFFER_SIZE);
  158. JsonObject& config = root.createNestedObject("config");
  159. _haSendSwitch(index, config);
  160. if (index == 0) output += "\n\n" + switchType + ":";
  161. output += "\n";
  162. bool first = true;
  163. for (auto kv : config) {
  164. if (first) {
  165. output += " - ";
  166. first = false;
  167. } else {
  168. output += " ";
  169. }
  170. output += kv.key;
  171. output += ": ";
  172. if (strncmp(kv.key, "payload_", strlen("payload_")) == 0) {
  173. output += _haFixPayload(kv.value.as<String>());
  174. } else {
  175. output += kv.value.as<String>();
  176. }
  177. output += "\n";
  178. }
  179. output += " ";
  180. root.remove("config");
  181. root["haConfig"] = output;
  182. }
  183. #if SENSOR_SUPPORT
  184. void _haSensorYaml(unsigned char index, JsonObject& root) {
  185. String output;
  186. output.reserve(HA_YAML_BUFFER_SIZE);
  187. JsonObject& config = root.createNestedObject("config");
  188. _haSendMagnitude(index, config);
  189. if (index == 0) output += "\n\nsensor:";
  190. output += "\n";
  191. bool first = true;
  192. for (auto kv : config) {
  193. if (first) {
  194. output += " - ";
  195. first = false;
  196. } else {
  197. output += " ";
  198. }
  199. String value = kv.value.as<String>();
  200. value.replace("%", "'%'");
  201. output += kv.key;
  202. output += ": ";
  203. output += value;
  204. output += "\n";
  205. }
  206. output += " ";
  207. root.remove("config");
  208. root["haConfig"] = output;
  209. }
  210. #endif // SENSOR_SUPPORT
  211. void _haGetDeviceConfig(JsonObject& config) {
  212. config.createNestedArray("identifiers").add(getIdentifier());
  213. config["name"] = getSetting("desc", getSetting("hostname"));
  214. config["manufacturer"] = MANUFACTURER;
  215. config["model"] = DEVICE;
  216. config["sw_version"] = String(APP_NAME) + " " + APP_VERSION + " (" + getCoreVersion() + ")";
  217. }
  218. void _haSend() {
  219. // Pending message to send?
  220. if (!_haSendFlag) return;
  221. // Are we connected?
  222. if (!mqttConnected()) return;
  223. DEBUG_MSG_P(PSTR("[HA] Sending autodiscovery MQTT message\n"));
  224. // Get common device config
  225. ha_config_t config;
  226. // Send messages
  227. _haSendSwitches(config);
  228. #if SENSOR_SUPPORT
  229. _haSendMagnitudes(config);
  230. #endif
  231. _haSendFlag = false;
  232. }
  233. void _haConfigure() {
  234. bool enabled = getSetting("haEnabled", HOMEASSISTANT_ENABLED).toInt() == 1;
  235. _haSendFlag = (enabled != _haEnabled);
  236. _haEnabled = enabled;
  237. _haSend();
  238. }
  239. #if WEB_SUPPORT
  240. bool _haWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  241. return (strncmp(key, "ha", 2) == 0);
  242. }
  243. void _haWebSocketOnVisible(JsonObject& root) {
  244. root["haVisible"] = 1;
  245. }
  246. void _haWebSocketOnConnected(JsonObject& root) {
  247. root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX);
  248. root["haEnabled"] = getSetting("haEnabled", HOMEASSISTANT_ENABLED).toInt() == 1;
  249. }
  250. void _haWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
  251. if (strcmp(action, "haconfig") == 0) {
  252. ws_on_send_callback_list_t callbacks;
  253. #if SENSOR_SUPPORT
  254. callbacks.reserve(magnitudeCount() + relayCount());
  255. #else
  256. callbacks.reserve(relayCount());
  257. #endif // SENSOR_SUPPORT
  258. {
  259. for (unsigned char idx=0; idx<relayCount(); ++idx) {
  260. callbacks.push_back([idx](JsonObject& root) {
  261. _haSwitchYaml(idx, root);
  262. });
  263. }
  264. }
  265. #if SENSOR_SUPPORT
  266. {
  267. for (unsigned char idx=0; idx<magnitudeCount(); ++idx) {
  268. callbacks.push_back([idx](JsonObject& root) {
  269. _haSensorYaml(idx, root);
  270. });
  271. }
  272. }
  273. #endif // SENSOR_SUPPORT
  274. if (callbacks.size()) wsPostSequence(client_id, std::move(callbacks));
  275. }
  276. }
  277. #endif // WEB_SUPPORT
  278. #if TERMINAL_SUPPORT
  279. void _haInitCommands() {
  280. terminalRegisterCommand(F("HA.CONFIG"), [](Embedis* e) {
  281. for (unsigned char idx=0; idx<relayCount(); ++idx) {
  282. DynamicJsonBuffer jsonBuffer(1024);
  283. JsonObject& root = jsonBuffer.createObject();
  284. _haSwitchYaml(idx, root);
  285. DEBUG_MSG(root["haConfig"].as<String>().c_str());
  286. }
  287. #if SENSOR_SUPPORT
  288. for (unsigned char idx=0; idx<magnitudeCount(); ++idx) {
  289. DynamicJsonBuffer jsonBuffer(1024);
  290. JsonObject& root = jsonBuffer.createObject();
  291. _haSensorYaml(idx, root);
  292. DEBUG_MSG(root["haConfig"].as<String>().c_str());
  293. }
  294. #endif // SENSOR_SUPPORT
  295. DEBUG_MSG("\n");
  296. terminalOK();
  297. });
  298. terminalRegisterCommand(F("HA.SEND"), [](Embedis* e) {
  299. setSetting("haEnabled", "1");
  300. _haConfigure();
  301. #if WEB_SUPPORT
  302. wsPost(_haWebSocketOnConnected);
  303. #endif
  304. terminalOK();
  305. });
  306. terminalRegisterCommand(F("HA.CLEAR"), [](Embedis* e) {
  307. setSetting("haEnabled", "0");
  308. _haConfigure();
  309. #if WEB_SUPPORT
  310. wsPost(_haWebSocketOnConnected);
  311. #endif
  312. terminalOK();
  313. });
  314. }
  315. #endif
  316. // -----------------------------------------------------------------------------
  317. void haSetup() {
  318. _haConfigure();
  319. #if WEB_SUPPORT
  320. wsRegister()
  321. .onVisible(_haWebSocketOnVisible)
  322. .onConnected(_haWebSocketOnConnected)
  323. .onAction(_haWebSocketOnAction)
  324. .onKeyCheck(_haWebSocketOnKeyCheck);
  325. #endif
  326. #if TERMINAL_SUPPORT
  327. _haInitCommands();
  328. #endif
  329. // On MQTT connect check if we have something to send
  330. mqttRegister([](unsigned int type, const char * topic, const char * payload) {
  331. if (type == MQTT_CONNECT_EVENT) _haSend();
  332. if (type == MQTT_DISCONNECT_EVENT) _haSendFlag = false;
  333. });
  334. // Main callbacks
  335. espurnaRegisterReload(_haConfigure);
  336. }
  337. #endif // HOMEASSISTANT_SUPPORT