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.

996 lines
25 KiB

  1. /*
  2. HOME ASSISTANT MODULE
  3. Original module
  4. Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
  5. Reworked queueing and RAM usage reduction
  6. Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
  7. */
  8. #include "espurna.h"
  9. #if HOMEASSISTANT_SUPPORT
  10. #include "light.h"
  11. #include "mqtt.h"
  12. #include "relay.h"
  13. #include "sensor.h"
  14. #include "ws.h"
  15. #include <ArduinoJson.h>
  16. #include <forward_list>
  17. #include <memory>
  18. namespace homeassistant {
  19. // Output is supposed to be used as both part of the MQTT config topic and the `uniq_id` field
  20. // TODO: manage UTF8 strings? in case we somehow receive `desc`, like it was done originally
  21. String normalize_ascii(String&& value, bool lower = false) {
  22. auto* ptr = const_cast<char*>(value.c_str());
  23. for (;;) {
  24. switch (*ptr) {
  25. case '\0':
  26. goto return_value;
  27. case '0' ... '9':
  28. case 'a' ... 'z':
  29. break;
  30. case 'A' ... 'Z':
  31. if (lower) {
  32. *ptr += 32;
  33. }
  34. break;
  35. default:
  36. *ptr = '_';
  37. break;
  38. }
  39. ++ptr;
  40. }
  41. return_value:
  42. return std::move(value);
  43. }
  44. // Common data used across the discovery payloads.
  45. // ref. https://developers.home-assistant.io/docs/entity_registry_index/
  46. class Device {
  47. public:
  48. struct Strings {
  49. Strings() = delete;
  50. Strings(const Strings&) = delete;
  51. Strings(Strings&&) = default;
  52. Strings(String&& prefix_, String&& name_, const String& identifier_, const String& version_, const String& manufacturer_, const String& device_) :
  53. prefix(std::move(prefix_)),
  54. name(std::move(name_)),
  55. identifier(identifier_),
  56. version(version_),
  57. manufacturer(manufacturer_),
  58. device(device_)
  59. {
  60. name = normalize_ascii(std::move(name));
  61. identifier = normalize_ascii(std::move(identifier), true);
  62. }
  63. String prefix;
  64. String name;
  65. String identifier;
  66. String version;
  67. String manufacturer;
  68. String device;
  69. };
  70. using StringsPtr = std::unique_ptr<Strings>;
  71. static constexpr size_t BufferSize { JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(5) };
  72. using Buffer = StaticJsonBuffer<BufferSize>;
  73. using BufferPtr = std::unique_ptr<Buffer>;
  74. Device() = delete;
  75. Device(const Device&) = delete;
  76. Device(Device&&) = default;
  77. Device(String&& prefix, String&& name, const String& identifier, const String& version, const String& manufacturer, const String& device) :
  78. _strings(std::make_unique<Strings>(std::move(prefix), std::move(name), identifier, version, manufacturer, device)),
  79. _buffer(std::make_unique<Buffer>()),
  80. _root(_buffer->createObject())
  81. {
  82. JsonArray& ids = _root.createNestedArray("ids");
  83. ids.add(_strings->identifier.c_str());
  84. _root["name"] = _strings->name.c_str();
  85. _root["sw"] = _strings->version.c_str();
  86. _root["mf"] = _strings->manufacturer.c_str();
  87. _root["mdl"] = _strings->device.c_str();
  88. }
  89. const String& name() const {
  90. return _strings->name;
  91. }
  92. const String& prefix() const {
  93. return _strings->prefix;
  94. }
  95. const String& identifier() const {
  96. return _strings->identifier;
  97. }
  98. JsonObject& root() {
  99. return _root;
  100. }
  101. private:
  102. StringsPtr _strings;
  103. BufferPtr _buffer;
  104. JsonObject& _root;
  105. };
  106. using DevicePtr = std::unique_ptr<Device>;
  107. using JsonBufferPtr = std::unique_ptr<DynamicJsonBuffer>;
  108. class Context {
  109. public:
  110. Context() = delete;
  111. Context(DevicePtr&& device, size_t capacity) :
  112. _device(std::move(device)),
  113. _capacity(capacity)
  114. {}
  115. const String& name() const {
  116. return _device->name();
  117. }
  118. const String& prefix() const {
  119. return _device->prefix();
  120. }
  121. const String& identifier() const {
  122. return _device->identifier();
  123. }
  124. JsonObject& device() {
  125. return _device->root();
  126. }
  127. void reset() {
  128. _json = std::make_unique<DynamicJsonBuffer>(_capacity);
  129. }
  130. size_t capacity() const {
  131. return _capacity;
  132. }
  133. size_t size() {
  134. if (_json) {
  135. return _json->size();
  136. }
  137. return 0;
  138. }
  139. JsonObject& makeObject() {
  140. if (!_json) {
  141. reset();
  142. }
  143. return _json->createObject();
  144. }
  145. private:
  146. String _prefix;
  147. DevicePtr _device;
  148. JsonBufferPtr _json;
  149. size_t _capacity { 0ul };
  150. };
  151. Context makeContext() {
  152. auto device = std::make_unique<Device>(
  153. getSetting("haPrefix", HOMEASSISTANT_PREFIX),
  154. getSetting("hostname", getIdentifier()),
  155. getIdentifier(),
  156. getVersion(),
  157. getManufacturer(),
  158. getDevice()
  159. );
  160. return Context(std::move(device), 2048ul);
  161. }
  162. String quote(String&& value) {
  163. if (value.equalsIgnoreCase("y")
  164. || value.equalsIgnoreCase("n")
  165. || value.equalsIgnoreCase("yes")
  166. || value.equalsIgnoreCase("no")
  167. || value.equalsIgnoreCase("true")
  168. || value.equalsIgnoreCase("false")
  169. || value.equalsIgnoreCase("on")
  170. || value.equalsIgnoreCase("off")
  171. ) {
  172. String result;
  173. result.reserve(value.length() + 2);
  174. result += '"';
  175. result += value;
  176. result += '"';
  177. return result;
  178. }
  179. return std::move(value);
  180. }
  181. // - Discovery object is expected to accept Context reference as input
  182. // (and all implementations do just that)
  183. // - topic() & message() return refs, since those *may* be called multiple times before advancing to the next 'entity'
  184. // - We use short-hand names right away, since we don't expect this to be used to generate yaml
  185. // - In case the object uses the JSON makeObject() as state, make sure we don't use it (state)
  186. // and the object itself after next() or ok() return false
  187. // - Make sure JSON state is not created on construction, but lazy-loaded as soon as it is needed.
  188. // Meaning, we don't cause invalid refs immediatly when there are more than 1 discovery object present and we reset the storage.
  189. class Discovery {
  190. public:
  191. virtual ~Discovery() {
  192. }
  193. virtual bool ok() const = 0;
  194. virtual const String& topic() = 0;
  195. virtual const String& message() = 0;
  196. virtual bool next() = 0;
  197. };
  198. #if RELAY_SUPPORT
  199. struct RelayContext {
  200. String availability;
  201. String payload_available;
  202. String payload_not_available;
  203. String payload_on;
  204. String payload_off;
  205. };
  206. RelayContext makeRelayContext() {
  207. return {
  208. mqttTopic(MQTT_TOPIC_STATUS, false),
  209. quote(mqttPayloadStatus(true)),
  210. quote(mqttPayloadStatus(false)),
  211. quote(relayPayload(PayloadStatus::On)),
  212. quote(relayPayload(PayloadStatus::Off))
  213. };
  214. }
  215. class RelayDiscovery : public Discovery {
  216. public:
  217. RelayDiscovery() = delete;
  218. explicit RelayDiscovery(Context& ctx) :
  219. _ctx(ctx),
  220. _relay(makeRelayContext()),
  221. _relays(relayCount())
  222. {
  223. if (!_relays) {
  224. return;
  225. }
  226. auto& json = root();
  227. json["dev"] = _ctx.device();
  228. json["avty_t"] = _relay.availability.c_str();
  229. json["pl_avail"] = _relay.payload_available.c_str();
  230. json["pl_not_avail"] = _relay.payload_not_available.c_str();
  231. json["pl_on"] = _relay.payload_on.c_str();
  232. json["pl_off"] = _relay.payload_off.c_str();
  233. }
  234. JsonObject& root() {
  235. if (!_root) {
  236. _root = &_ctx.makeObject();
  237. }
  238. return *_root;
  239. }
  240. bool ok() const override {
  241. return (_relays) && (_index < _relays);
  242. }
  243. const String& uniqueId() {
  244. if (!_unique_id.length()) {
  245. _unique_id = _ctx.identifier() + '_' + F("relay") + '_' + _index;
  246. }
  247. return _unique_id;
  248. }
  249. const String& topic() override {
  250. if (!_topic.length()) {
  251. _topic = _ctx.prefix();
  252. _topic += F("/switch/");
  253. _topic += uniqueId();
  254. _topic += F("/config");
  255. }
  256. return _topic;
  257. }
  258. const String& message() override {
  259. if (!_message.length()) {
  260. auto& json = root();
  261. json["uniq_id"] = uniqueId();
  262. json["name"] = _ctx.name() + ' ' + _index;
  263. json["stat_t"] = mqttTopic(MQTT_TOPIC_RELAY, _index, false);
  264. json["cmd_t"] = mqttTopic(MQTT_TOPIC_RELAY, _index, true);
  265. json.printTo(_message);
  266. }
  267. return _message;
  268. }
  269. bool next() override {
  270. if (_index < _relays) {
  271. auto current = _index;
  272. ++_index;
  273. if ((_index > current) && (_index < _relays)) {
  274. _unique_id = "";
  275. _topic = "";
  276. _message = "";
  277. return true;
  278. }
  279. }
  280. return false;
  281. }
  282. private:
  283. Context& _ctx;
  284. JsonObject* _root { nullptr };
  285. RelayContext _relay;
  286. unsigned char _index { 0u };
  287. unsigned char _relays { 0u };
  288. String _unique_id;
  289. String _topic;
  290. String _message;
  291. };
  292. #endif
  293. // Example payload:
  294. // {
  295. // "brightness": 255,
  296. // "color_temp": 155,
  297. // "color": {
  298. // "r": 255,
  299. // "g": 180,
  300. // "b": 200,
  301. // "x": 0.406,
  302. // "y": 0.301,
  303. // "h": 344.0,
  304. // "s": 29.412
  305. // },
  306. // "effect": "colorloop",
  307. // "state": "ON",
  308. // "transition": 2,
  309. // "white_value": 150
  310. // }
  311. // Notice that we only support JSON schema payloads, leaving it to the user to configure special
  312. // per-channel topics, as those don't really fit into the HASS idea of lights controls for a single device
  313. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  314. class LightDiscovery : public Discovery {
  315. public:
  316. explicit LightDiscovery(Context& ctx) :
  317. _ctx(ctx)
  318. {}
  319. JsonObject& root() {
  320. if (!_root) {
  321. _root = &_ctx.makeObject();
  322. }
  323. return *_root;
  324. }
  325. bool ok() const override {
  326. return true;
  327. }
  328. bool next() override {
  329. return false;
  330. }
  331. const String& uniqueId() {
  332. if (!_unique_id.length()) {
  333. _unique_id = _ctx.identifier() + '_' + F("light");
  334. }
  335. return _unique_id;
  336. }
  337. const String& topic() override {
  338. if (!_topic.length()) {
  339. _topic = _ctx.prefix();
  340. _topic += F("/light/");
  341. _topic += uniqueId();
  342. _topic += F("/config");
  343. }
  344. return _topic;
  345. }
  346. const String& message() override {
  347. if (!_message.length()) {
  348. auto& json = root();
  349. json["schema"] = "json";
  350. json["uniq_id"] = uniqueId();
  351. json["name"] = _ctx.name() + ' ' + F("Light");
  352. json["stat_t"] = mqttTopic(MQTT_TOPIC_LIGHT_JSON, false);
  353. json["cmd_t"] = mqttTopic(MQTT_TOPIC_LIGHT_JSON, true);
  354. json["avty_t"] = mqttTopic(MQTT_TOPIC_STATUS, false);
  355. json["pl_avail"] = quote(mqttPayloadStatus(true));
  356. json["pl_not_avail"] = quote(mqttPayloadStatus(false));
  357. // send `true` for every payload we support sending / receiving
  358. // already enabled by default: "state", "transition"
  359. json["brightness"] = true;
  360. // Note that since we send back the values immediately, HS mode sliders
  361. // *will jump*, as calculations of input do not always match the output.
  362. // (especially, when gamma table is used, as we modify the results)
  363. // In case or RGB, channel values input is expected to match the output exactly.
  364. if (lightHasColor()) {
  365. if (lightUseRGB()) {
  366. json["rgb"] = true;
  367. } else {
  368. json["hs"] = true;
  369. }
  370. }
  371. // Mired is only an input, we never send this value back
  372. // (...besides the internally pinned value, ref. MQTT_TOPIC_MIRED. not used here though)
  373. // - in RGB mode, we convert the temperature into a specific color
  374. // - in CCT mode, white channels are used
  375. if (lightHasColor() || lightUseCCT()) {
  376. auto range = lightMiredsRange();
  377. json["min_mirs"] = range.cold();
  378. json["max_mirs"] = range.warm();
  379. json["color_temp"] = true;
  380. }
  381. json.printTo(_message);
  382. }
  383. return _message;
  384. }
  385. private:
  386. Context& _ctx;
  387. JsonObject* _root { nullptr };
  388. String _unique_id;
  389. String _topic;
  390. String _message;
  391. };
  392. bool heartbeat(heartbeat::Mask mask) {
  393. // TODO: mask json payload specifically?
  394. // or, find a way to detach masking from the system setting / don't use heartbeat timer
  395. if (mask & heartbeat::Report::Light) {
  396. DynamicJsonBuffer buffer(512);
  397. JsonObject& root = buffer.createObject();
  398. auto state = lightState();
  399. root["state"] = state ? "ON" : "OFF";
  400. if (state) {
  401. root["brightness"] = lightBrightness();
  402. if (lightUseCCT()) {
  403. root["white_value"] = lightColdWhite();
  404. }
  405. if (lightColor()) {
  406. auto& color = root.createNestedObject("color");
  407. if (lightUseRGB()) {
  408. auto rgb = lightRgb();
  409. color["r"] = rgb.red();
  410. color["g"] = rgb.green();
  411. color["b"] = rgb.blue();
  412. } else {
  413. auto hsv = lightHsv();
  414. color["h"] = hsv.hue();
  415. color["s"] = hsv.saturation();
  416. }
  417. }
  418. }
  419. String message;
  420. root.printTo(message);
  421. String topic = mqttTopic(MQTT_TOPIC_LIGHT_JSON, false);
  422. mqttSendRaw(topic.c_str(), message.c_str(), false);
  423. }
  424. return true;
  425. }
  426. void publishLightJson() {
  427. heartbeat(static_cast<heartbeat::Mask>(heartbeat::Report::Light));
  428. }
  429. void receiveLightJson(char* payload) {
  430. DynamicJsonBuffer buffer(1024);
  431. JsonObject& root = buffer.parseObject(payload);
  432. if (!root.success()) {
  433. return;
  434. }
  435. if (!root.containsKey("state")) {
  436. return;
  437. }
  438. auto state = root["state"].as<String>();
  439. if (state == F("ON")) {
  440. lightState(true);
  441. } else if (state == F("OFF")) {
  442. lightState(false);
  443. } else {
  444. return;
  445. }
  446. unsigned long transition { lightTransitionTime() };
  447. if (root.containsKey("transition")) {
  448. auto seconds = root["transition"].as<float>();
  449. if (seconds > 0) {
  450. transition = static_cast<unsigned long>(seconds * 1000.0);
  451. }
  452. }
  453. if (root.containsKey("color_temp")) {
  454. lightMireds(root["color_temp"].as<long>());
  455. }
  456. if (root.containsKey("brightness")) {
  457. lightBrightness(root["brightness"].as<long>());
  458. }
  459. if (lightHasColor() && root.containsKey("color")) {
  460. JsonObject& color = root["color"];
  461. if (lightUseRGB()) {
  462. lightRgb({
  463. color["r"].as<long>(),
  464. color["g"].as<long>(),
  465. color["b"].as<long>()});
  466. } else {
  467. lightHs(
  468. color["h"].as<long>(),
  469. color["s"].as<long>());
  470. }
  471. }
  472. if (lightUseCCT() && root.containsKey("white_value")) {
  473. lightColdWhite(root["white_value"].as<long>());
  474. }
  475. lightUpdate({transition, lightTransitionStep()});
  476. }
  477. #endif
  478. #if SENSOR_SUPPORT
  479. class SensorDiscovery : public Discovery {
  480. public:
  481. SensorDiscovery() = delete;
  482. explicit SensorDiscovery(Context& ctx) :
  483. _ctx(ctx),
  484. _magnitudes(magnitudeCount())
  485. {}
  486. JsonObject& root() {
  487. if (!_root) {
  488. _root = &_ctx.makeObject();
  489. }
  490. return *_root;
  491. }
  492. bool ok() const {
  493. return _index < _magnitudes;
  494. }
  495. const String& topic() override {
  496. if (!_topic.length()) {
  497. _topic = _ctx.prefix();
  498. _topic += F("/sensor/");
  499. _topic += uniqueId();
  500. _topic += F("/config");
  501. }
  502. return _topic;
  503. }
  504. const String& message() override {
  505. if (!_message.length()) {
  506. auto& json = root();
  507. json["dev"] = _ctx.device();
  508. json["uniq_id"] = uniqueId();
  509. json["name"] = _ctx.name() + ' ' + name() + ' ' + localId();
  510. json["stat_t"] = mqttTopic(magnitudeTopicIndex(_index).c_str(), false);
  511. json["unit_of_meas"] = magnitudeUnits(_index);
  512. json.printTo(_message);
  513. }
  514. return _message;
  515. }
  516. const String& name() {
  517. if (!_name.length()) {
  518. _name = magnitudeTopic(magnitudeType(_index));
  519. }
  520. return _name;
  521. }
  522. unsigned char localId() const {
  523. return magnitudeIndex(_index);
  524. }
  525. const String& uniqueId() {
  526. if (!_unique_id.length()) {
  527. _unique_id = _ctx.identifier() + '_' + name() + '_' + localId();
  528. }
  529. return _unique_id;
  530. }
  531. bool next() override {
  532. if (_index < _magnitudes) {
  533. auto current = _index;
  534. ++_index;
  535. if ((_index > current) && (_index < _magnitudes)) {
  536. _unique_id = "";
  537. _name = "";
  538. _topic = "";
  539. _message = "";
  540. return true;
  541. }
  542. }
  543. return false;
  544. }
  545. private:
  546. Context& _ctx;
  547. JsonObject* _root { nullptr };
  548. unsigned char _index { 0u };
  549. unsigned char _magnitudes { 0u };
  550. String _unique_id;
  551. String _name;
  552. String _topic;
  553. String _message;
  554. };
  555. #endif
  556. // Reworked discovery class. Try to send and wait for MQTT QoS 1 publish ACK to continue.
  557. // Topic and message are generated on demand and most of JSON payload is cached for re-use to save RAM.
  558. class DiscoveryTask {
  559. public:
  560. using Entity = std::unique_ptr<Discovery>;
  561. using Entities = std::forward_list<Entity>;
  562. static constexpr int Retries { 5 };
  563. static constexpr unsigned long WaitShortMs { 100ul };
  564. static constexpr unsigned long WaitLongMs { 1000ul };
  565. DiscoveryTask(bool enabled) :
  566. _enabled(enabled)
  567. {}
  568. void add(Entity&& entity) {
  569. _entities.push_front(std::move(entity));
  570. }
  571. template <typename T>
  572. void add() {
  573. _entities.push_front(std::make_unique<T>(_ctx));
  574. }
  575. bool retry() {
  576. if (_retry < 0) {
  577. return false;
  578. }
  579. return (--_retry > 0);
  580. }
  581. Context& context() {
  582. return _ctx;
  583. }
  584. bool done() const {
  585. return _entities.empty();
  586. }
  587. bool ok() const {
  588. if ((_retry > 0) && !_entities.empty()) {
  589. auto& entity = _entities.front();
  590. return entity->ok();
  591. }
  592. return false;
  593. }
  594. template <typename T>
  595. bool send(T&& action) {
  596. while (!_entities.empty()) {
  597. auto& entity = _entities.front();
  598. if (!entity->ok()) {
  599. _entities.pop_front();
  600. _ctx.reset();
  601. continue;
  602. }
  603. const auto* topic = entity->topic().c_str();
  604. const auto* msg = _enabled
  605. ? entity->message().c_str()
  606. : "";
  607. if (action(topic, msg)) {
  608. if (!entity->next()) {
  609. _retry = Retries;
  610. _entities.pop_front();
  611. _ctx.reset();
  612. }
  613. return true;
  614. }
  615. return false;
  616. }
  617. return false;
  618. }
  619. private:
  620. bool _enabled { false };
  621. int _retry { Retries };
  622. Context _ctx { makeContext() };
  623. Entities _entities;
  624. };
  625. namespace internal {
  626. using TaskPtr = std::shared_ptr<DiscoveryTask>;
  627. using FlagPtr = std::shared_ptr<bool>;
  628. bool retain { false };
  629. bool enabled { false };
  630. enum class State {
  631. Initial,
  632. Pending,
  633. Sent
  634. };
  635. State state { State::Initial };
  636. Ticker timer;
  637. void send(TaskPtr ptr, FlagPtr flag_ptr);
  638. void stop(bool done) {
  639. timer.detach();
  640. if (done) {
  641. DEBUG_MSG_P(PSTR("[HA] Stopping discovery\n"));
  642. state = State::Sent;
  643. } else {
  644. DEBUG_MSG_P(PSTR("[HA] Discovery error\n"));
  645. state = State::Pending;
  646. }
  647. }
  648. void schedule(unsigned long wait, TaskPtr ptr, FlagPtr flag_ptr) {
  649. internal::timer.once_ms_scheduled(wait, [ptr, flag_ptr]() {
  650. send(ptr, flag_ptr);
  651. });
  652. }
  653. void schedule(TaskPtr ptr, FlagPtr flag_ptr) {
  654. schedule(DiscoveryTask::WaitShortMs, ptr, flag_ptr);
  655. }
  656. void schedule(TaskPtr ptr) {
  657. schedule(DiscoveryTask::WaitShortMs, ptr, std::make_shared<bool>(true));
  658. }
  659. void send(TaskPtr ptr, FlagPtr flag_ptr) {
  660. auto& task = *ptr;
  661. if (!mqttConnected() || task.done()) {
  662. stop(true);
  663. return;
  664. }
  665. auto& flag = *flag_ptr;
  666. if (!flag) {
  667. if (task.retry()) {
  668. schedule(ptr, flag_ptr);
  669. } else {
  670. stop(false);
  671. }
  672. return;
  673. }
  674. uint16_t pid { 0u };
  675. auto res = task.send([&](const char* topic, const char* message) {
  676. pid = ::mqttSendRaw(topic, message, internal::retain, 1);
  677. return pid > 0;
  678. });
  679. #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
  680. // - async fails when disconneted and when it's buffers are filled, which should be resolved after $LATENCY
  681. // and the time it takes for the lwip to process it. future versions use queue, but could still fail when low on RAM
  682. // - lwmqtt will fail when disconnected (already checked above) and *will* disconnect in case publish fails.
  683. // ::publish() will wait for the puback, so we don't have to do it ourselves. not tested.
  684. // - pubsub will fail when it can't buffer the payload *or* the underlying WiFiClient calls fail. also not tested.
  685. if (res) {
  686. flag = false;
  687. mqttOnPublish(pid, [flag_ptr]() {
  688. (*flag_ptr) = true;
  689. });
  690. }
  691. #endif
  692. auto wait = res
  693. ? DiscoveryTask::WaitShortMs
  694. : DiscoveryTask::WaitLongMs;
  695. if (res || task.retry()) {
  696. schedule(wait, ptr, flag_ptr);
  697. return;
  698. }
  699. stop(false);
  700. }
  701. } // namespace internal
  702. void publishDiscovery() {
  703. if (!mqttConnected() || internal::timer.active() || (internal::state != internal::State::Pending)) {
  704. return;
  705. }
  706. auto task = std::make_shared<DiscoveryTask>(internal::enabled);
  707. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  708. task->add<LightDiscovery>();
  709. #endif
  710. #if RELAY_SUPPORT
  711. task->add<RelayDiscovery>();
  712. #endif
  713. #if SENSOR_SUPPORT
  714. task->add<SensorDiscovery>();
  715. #endif
  716. // only happens when nothing is configured to do the add()
  717. if (task->done()) {
  718. return;
  719. }
  720. internal::schedule(task);
  721. }
  722. void configure() {
  723. bool current = internal::enabled;
  724. internal::enabled = getSetting("haEnabled", 1 == HOMEASSISTANT_ENABLED);
  725. internal::retain = getSetting("haRetain", 1 == HOMEASSISTANT_RETAIN);
  726. if (internal::enabled != current) {
  727. internal::state = internal::State::Pending;
  728. }
  729. homeassistant::publishDiscovery();
  730. }
  731. void mqttCallback(unsigned int type, const char* topic, char* payload) {
  732. if (MQTT_DISCONNECT_EVENT == type) {
  733. if (internal::state == internal::State::Sent) {
  734. internal::state = internal::State::Pending;
  735. }
  736. internal::timer.detach();
  737. return;
  738. }
  739. if (MQTT_CONNECT_EVENT == type) {
  740. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  741. ::mqttSubscribe(MQTT_TOPIC_LIGHT_JSON);
  742. #endif
  743. ::schedule_function(publishDiscovery);
  744. return;
  745. }
  746. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  747. if (type == MQTT_MESSAGE_EVENT) {
  748. String t = ::mqttMagnitude(topic);
  749. if (t.equals(MQTT_TOPIC_LIGHT_JSON)) {
  750. receiveLightJson(payload);
  751. }
  752. return;
  753. }
  754. #endif
  755. }
  756. namespace web {
  757. #if WEB_SUPPORT
  758. void onVisible(JsonObject& root) {
  759. root["haVisible"] = 1;
  760. }
  761. void onConnected(JsonObject& root) {
  762. root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX);
  763. root["haEnabled"] = getSetting("haEnabled", 1 == HOMEASSISTANT_ENABLED);
  764. root["haRetain"] = getSetting("haRetain", 1 == HOMEASSISTANT_RETAIN);
  765. }
  766. bool onKeyCheck(const char* key, JsonVariant& value) {
  767. return (strncmp(key, "ha", 2) == 0);
  768. }
  769. #endif
  770. } // namespace web
  771. } // namespace homeassistant
  772. // This module no longer implements .yaml generation, since we can't:
  773. // - use unique_id in the device config
  774. // - have abbreviated keys
  775. // - have mqtt reliably return the correct status & command payloads when it is disabled
  776. // (yet? needs reworked configuration section or making functions read settings directly)
  777. void haSetup() {
  778. #if WEB_SUPPORT
  779. wsRegister()
  780. .onVisible(homeassistant::web::onVisible)
  781. .onConnected(homeassistant::web::onConnected)
  782. .onKeyCheck(homeassistant::web::onKeyCheck);
  783. #endif
  784. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  785. lightSetReportListener(homeassistant::publishLightJson);
  786. mqttHeartbeat(homeassistant::heartbeat);
  787. #endif
  788. mqttRegister(homeassistant::mqttCallback);
  789. #if TERMINAL_SUPPORT
  790. terminalRegisterCommand(F("HA.SEND"), [](const terminal::CommandContext& ctx) {
  791. using namespace homeassistant::internal;
  792. state = State::Pending;
  793. homeassistant::publishDiscovery();
  794. terminalOK(ctx);
  795. });
  796. #endif
  797. espurnaRegisterReload(homeassistant::configure);
  798. homeassistant::configure();
  799. }
  800. #endif // HOMEASSISTANT_SUPPORT