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.

441 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. config["platform"] = "mqtt";
  160. _haSendSwitch(index, config);
  161. if (index == 0) output += "\n\n" + switchType + ":";
  162. output += "\n";
  163. bool first = true;
  164. for (auto kv : config) {
  165. if (first) {
  166. output += " - ";
  167. first = false;
  168. } else {
  169. output += " ";
  170. }
  171. output += kv.key;
  172. output += ": ";
  173. if (strncmp(kv.key, "payload_", strlen("payload_")) == 0) {
  174. output += _haFixPayload(kv.value.as<String>());
  175. } else {
  176. output += kv.value.as<String>();
  177. }
  178. output += "\n";
  179. }
  180. output += " ";
  181. root.remove("config");
  182. root["haConfig"] = output;
  183. }
  184. #if SENSOR_SUPPORT
  185. void _haSensorYaml(unsigned char index, JsonObject& root) {
  186. String output;
  187. output.reserve(HA_YAML_BUFFER_SIZE);
  188. JsonObject& config = root.createNestedObject("config");
  189. config["platform"] = "mqtt";
  190. _haSendMagnitude(index, config);
  191. if (index == 0) output += "\n\nsensor:";
  192. output += "\n";
  193. bool first = true;
  194. for (auto kv : config) {
  195. if (first) {
  196. output += " - ";
  197. first = false;
  198. } else {
  199. output += " ";
  200. }
  201. String value = kv.value.as<String>();
  202. value.replace("%", "'%'");
  203. output += kv.key;
  204. output += ": ";
  205. output += value;
  206. output += "\n";
  207. }
  208. output += " ";
  209. root.remove("config");
  210. root["haConfig"] = output;
  211. }
  212. #endif // SENSOR_SUPPORT
  213. void _haGetDeviceConfig(JsonObject& config) {
  214. config.createNestedArray("identifiers").add(getIdentifier());
  215. config["name"] = getSetting("desc", getSetting("hostname"));
  216. config["manufacturer"] = MANUFACTURER;
  217. config["model"] = DEVICE;
  218. config["sw_version"] = String(APP_NAME) + " " + APP_VERSION + " (" + getCoreVersion() + ")";
  219. }
  220. void _haSend() {
  221. // Pending message to send?
  222. if (!_haSendFlag) return;
  223. // Are we connected?
  224. if (!mqttConnected()) return;
  225. DEBUG_MSG_P(PSTR("[HA] Sending autodiscovery MQTT message\n"));
  226. // Get common device config
  227. ha_config_t config;
  228. // Send messages
  229. _haSendSwitches(config);
  230. #if SENSOR_SUPPORT
  231. _haSendMagnitudes(config);
  232. #endif
  233. _haSendFlag = false;
  234. }
  235. void _haConfigure() {
  236. bool enabled = getSetting("haEnabled", HOMEASSISTANT_ENABLED).toInt() == 1;
  237. _haSendFlag = (enabled != _haEnabled);
  238. _haEnabled = enabled;
  239. _haSend();
  240. }
  241. #if WEB_SUPPORT
  242. bool _haWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  243. return (strncmp(key, "ha", 2) == 0);
  244. }
  245. void _haWebSocketOnVisible(JsonObject& root) {
  246. root["haVisible"] = 1;
  247. }
  248. void _haWebSocketOnConnected(JsonObject& root) {
  249. root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX);
  250. root["haEnabled"] = getSetting("haEnabled", HOMEASSISTANT_ENABLED).toInt() == 1;
  251. }
  252. void _haWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
  253. if (strcmp(action, "haconfig") == 0) {
  254. ws_on_send_callback_list_t callbacks;
  255. #if SENSOR_SUPPORT
  256. callbacks.reserve(magnitudeCount() + relayCount());
  257. #else
  258. callbacks.reserve(relayCount());
  259. #endif // SENSOR_SUPPORT
  260. {
  261. for (unsigned char idx=0; idx<relayCount(); ++idx) {
  262. callbacks.push_back([idx](JsonObject& root) {
  263. _haSwitchYaml(idx, root);
  264. });
  265. }
  266. }
  267. #if SENSOR_SUPPORT
  268. {
  269. for (unsigned char idx=0; idx<magnitudeCount(); ++idx) {
  270. callbacks.push_back([idx](JsonObject& root) {
  271. _haSensorYaml(idx, root);
  272. });
  273. }
  274. }
  275. #endif // SENSOR_SUPPORT
  276. if (callbacks.size()) wsPostSequence(client_id, std::move(callbacks));
  277. }
  278. }
  279. #endif // WEB_SUPPORT
  280. #if TERMINAL_SUPPORT
  281. void _haInitCommands() {
  282. terminalRegisterCommand(F("HA.CONFIG"), [](Embedis* e) {
  283. for (unsigned char idx=0; idx<relayCount(); ++idx) {
  284. DynamicJsonBuffer jsonBuffer(1024);
  285. JsonObject& root = jsonBuffer.createObject();
  286. _haSwitchYaml(idx, root);
  287. DEBUG_MSG(root["haConfig"].as<String>().c_str());
  288. }
  289. #if SENSOR_SUPPORT
  290. for (unsigned char idx=0; idx<magnitudeCount(); ++idx) {
  291. DynamicJsonBuffer jsonBuffer(1024);
  292. JsonObject& root = jsonBuffer.createObject();
  293. _haSensorYaml(idx, root);
  294. DEBUG_MSG(root["haConfig"].as<String>().c_str());
  295. }
  296. #endif // SENSOR_SUPPORT
  297. DEBUG_MSG("\n");
  298. terminalOK();
  299. });
  300. terminalRegisterCommand(F("HA.SEND"), [](Embedis* e) {
  301. setSetting("haEnabled", "1");
  302. _haConfigure();
  303. #if WEB_SUPPORT
  304. wsPost(_haWebSocketOnConnected);
  305. #endif
  306. terminalOK();
  307. });
  308. terminalRegisterCommand(F("HA.CLEAR"), [](Embedis* e) {
  309. setSetting("haEnabled", "0");
  310. _haConfigure();
  311. #if WEB_SUPPORT
  312. wsPost(_haWebSocketOnConnected);
  313. #endif
  314. terminalOK();
  315. });
  316. }
  317. #endif
  318. // -----------------------------------------------------------------------------
  319. void haSetup() {
  320. _haConfigure();
  321. #if WEB_SUPPORT
  322. wsRegister()
  323. .onVisible(_haWebSocketOnVisible)
  324. .onConnected(_haWebSocketOnConnected)
  325. .onAction(_haWebSocketOnAction)
  326. .onKeyCheck(_haWebSocketOnKeyCheck);
  327. #endif
  328. #if TERMINAL_SUPPORT
  329. _haInitCommands();
  330. #endif
  331. // On MQTT connect check if we have something to send
  332. mqttRegister([](unsigned int type, const char * topic, const char * payload) {
  333. if (type == MQTT_CONNECT_EVENT) _haSend();
  334. if (type == MQTT_DISCONNECT_EVENT) _haSendFlag = false;
  335. });
  336. // Main callbacks
  337. espurnaRegisterReload(_haConfigure);
  338. }
  339. #endif // HOMEASSISTANT_SUPPORT