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.

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