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.

934 lines
23 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. // ref. SUPPORT_... flags throughout the light component
  358. // send `true` for every payload we support sending / receiving
  359. // already enabled by default: "state", "transition"
  360. // TODO: handle "rgb", "color_temp" and "white_value"
  361. json["brightness"] = true;
  362. if (lightHasColor()) {
  363. json["rgb"] = true;
  364. }
  365. if (lightHasColor() || lightUseCCT()) {
  366. json["max_mireds"] = LIGHT_WARMWHITE_MIRED;
  367. json["min_mireds"] = LIGHT_COLDWHITE_MIRED;
  368. json["color_temp"] = true;
  369. }
  370. json.printTo(_message);
  371. }
  372. return _message;
  373. }
  374. private:
  375. Context& _ctx;
  376. JsonObject* _root { nullptr };
  377. String _unique_id;
  378. String _topic;
  379. String _message;
  380. };
  381. void publishLightJson() {
  382. if (!mqttConnected()) {
  383. return;
  384. }
  385. DynamicJsonBuffer buffer(512);
  386. JsonObject& root = buffer.createObject();
  387. root["state"] = lightState() ? "ON" : "OFF";
  388. root["brightness"] = lightBrightness();
  389. String message;
  390. root.printTo(message);
  391. String topic = mqttTopic(MQTT_TOPIC_LIGHT_JSON, false);
  392. mqttSendRaw(topic.c_str(), message.c_str(), false);
  393. }
  394. void receiveLightJson(char* payload) {
  395. DynamicJsonBuffer buffer(1024);
  396. JsonObject& root = buffer.parseObject(payload);
  397. if (!root.success()) {
  398. return;
  399. }
  400. if (!root.containsKey("state")) {
  401. return;
  402. }
  403. auto state = root["state"].as<String>();
  404. if (state == F("ON")) {
  405. lightState(true);
  406. } else if (state == F("OFF")) {
  407. lightState(false);
  408. } else {
  409. return;
  410. }
  411. unsigned long transition { lightTransitionTime() };
  412. if (root.containsKey("transition")) {
  413. auto seconds = root["transition"].as<float>();
  414. if (seconds > 0) {
  415. transition = static_cast<unsigned long>(seconds * 1000.0);
  416. }
  417. }
  418. if (root.containsKey("brightness")) {
  419. lightBrightness(root["brightness"].as<long>());
  420. }
  421. // TODO: handle "rgb", "color_temp" and "white_value"
  422. lightUpdate({transition, lightTransitionStep()});
  423. }
  424. #endif
  425. #if SENSOR_SUPPORT
  426. class SensorDiscovery : public Discovery {
  427. public:
  428. SensorDiscovery() = delete;
  429. explicit SensorDiscovery(Context& ctx) :
  430. _ctx(ctx),
  431. _magnitudes(magnitudeCount())
  432. {}
  433. JsonObject& root() {
  434. if (!_root) {
  435. _root = &_ctx.makeObject();
  436. }
  437. return *_root;
  438. }
  439. bool ok() const {
  440. return _index < _magnitudes;
  441. }
  442. const String& topic() override {
  443. if (!_topic.length()) {
  444. _topic = _ctx.prefix();
  445. _topic += F("/sensor/");
  446. _topic += uniqueId();
  447. _topic += F("/config");
  448. }
  449. return _topic;
  450. }
  451. const String& message() override {
  452. if (!_message.length()) {
  453. auto& json = root();
  454. json["dev"] = _ctx.device();
  455. json["uniq_id"] = uniqueId();
  456. json["name"] = _ctx.name() + ' ' + name() + ' ' + localId();
  457. json["stat_t"] = mqttTopic(magnitudeTopicIndex(_index).c_str(), false);
  458. json["unit_of_meas"] = magnitudeUnits(_index);
  459. json.printTo(_message);
  460. }
  461. return _message;
  462. }
  463. const String& name() {
  464. if (!_name.length()) {
  465. _name = magnitudeTopic(magnitudeType(_index));
  466. }
  467. return _name;
  468. }
  469. unsigned char localId() const {
  470. return magnitudeIndex(_index);
  471. }
  472. const String& uniqueId() {
  473. if (!_unique_id.length()) {
  474. _unique_id = _ctx.identifier() + '_' + name() + '_' + localId();
  475. }
  476. return _unique_id;
  477. }
  478. bool next() override {
  479. if (_index < _magnitudes) {
  480. auto current = _index;
  481. ++_index;
  482. if ((_index > current) && (_index < _magnitudes)) {
  483. _unique_id = "";
  484. _name = "";
  485. _topic = "";
  486. _message = "";
  487. return true;
  488. }
  489. }
  490. return false;
  491. }
  492. private:
  493. Context& _ctx;
  494. JsonObject* _root { nullptr };
  495. unsigned char _index { 0u };
  496. unsigned char _magnitudes { 0u };
  497. String _unique_id;
  498. String _name;
  499. String _topic;
  500. String _message;
  501. };
  502. #endif
  503. // Reworked discovery class. Try to send and wait for MQTT QoS 1 publish ACK to continue.
  504. // Topic and message are generated on demand and most of JSON payload is cached for re-use to save RAM.
  505. class DiscoveryTask {
  506. public:
  507. using Entity = std::unique_ptr<Discovery>;
  508. using Entities = std::forward_list<Entity>;
  509. static constexpr int Retries { 5 };
  510. static constexpr unsigned long WaitShortMs { 100ul };
  511. static constexpr unsigned long WaitLongMs { 1000ul };
  512. DiscoveryTask(bool enabled) :
  513. _enabled(enabled)
  514. {}
  515. void add(Entity&& entity) {
  516. _entities.push_front(std::move(entity));
  517. }
  518. template <typename T>
  519. void add() {
  520. _entities.push_front(std::make_unique<T>(_ctx));
  521. }
  522. bool retry() {
  523. if (_retry < 0) {
  524. return false;
  525. }
  526. return (--_retry < 0);
  527. }
  528. Context& context() {
  529. return _ctx;
  530. }
  531. bool done() const {
  532. return _entities.empty();
  533. }
  534. bool ok() const {
  535. if ((_retry > 0) && !_entities.empty()) {
  536. auto& entity = _entities.front();
  537. return entity->ok();
  538. }
  539. return false;
  540. }
  541. template <typename T>
  542. bool send(T&& action) {
  543. while (!_entities.empty()) {
  544. auto& entity = _entities.front();
  545. if (!entity->ok()) {
  546. _entities.pop_front();
  547. _ctx.reset();
  548. continue;
  549. }
  550. const auto* topic = entity->topic().c_str();
  551. const auto* msg = _enabled
  552. ? entity->message().c_str()
  553. : "";
  554. if (action(topic, msg)) {
  555. if (!entity->next()) {
  556. _retry = Retries;
  557. _entities.pop_front();
  558. _ctx.reset();
  559. }
  560. return true;
  561. }
  562. return false;
  563. }
  564. return false;
  565. }
  566. private:
  567. bool _enabled { false };
  568. int _retry { Retries };
  569. Context _ctx { makeContext() };
  570. Entities _entities;
  571. };
  572. namespace internal {
  573. using TaskPtr = std::shared_ptr<DiscoveryTask>;
  574. using FlagPtr = std::shared_ptr<bool>;
  575. bool retain { false };
  576. bool enabled { false };
  577. enum class State {
  578. Initial,
  579. Pending,
  580. Sent
  581. };
  582. State state { State::Initial };
  583. Ticker timer;
  584. void send(TaskPtr ptr, FlagPtr flag_ptr);
  585. void stop(bool done) {
  586. timer.detach();
  587. state = done ? State::Sent : State::Pending;
  588. }
  589. void schedule(unsigned long wait, TaskPtr ptr, FlagPtr flag_ptr) {
  590. internal::timer.once_ms_scheduled(wait, [ptr, flag_ptr]() {
  591. send(ptr, flag_ptr);
  592. });
  593. }
  594. void schedule(TaskPtr ptr, FlagPtr flag_ptr) {
  595. schedule(DiscoveryTask::WaitShortMs, ptr, flag_ptr);
  596. }
  597. void schedule(TaskPtr ptr) {
  598. schedule(DiscoveryTask::WaitShortMs, ptr, std::make_shared<bool>(true));
  599. }
  600. void send(TaskPtr ptr, FlagPtr flag_ptr) {
  601. auto& task = *ptr;
  602. if (!mqttConnected() || task.done()) {
  603. stop(true);
  604. return;
  605. }
  606. auto& flag = *flag_ptr;
  607. if (!flag) {
  608. if (task.retry()) {
  609. schedule(ptr, flag_ptr);
  610. } else {
  611. stop(false);
  612. }
  613. return;
  614. }
  615. uint16_t pid { 0u };
  616. auto res = task.send([&](const char* topic, const char* message) {
  617. pid = ::mqttSendRaw(topic, message, internal::retain, 1);
  618. return pid > 0;
  619. });
  620. #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
  621. // - async fails when disconneted and when it's buffers are filled, which should be resolved after $LATENCY
  622. // and the time it takes for the lwip to process it. future versions use queue, but could still fail when low on RAM
  623. // - lwmqtt will fail when disconnected (already checked above) and *will* disconnect in case publish fails. publish funciton will
  624. // wait for the puback all by itself. not tested.
  625. // - pubsub will fail when it can't buffer the payload *or* the underlying wificlient fails. also not tested.
  626. if (res) {
  627. flag = false;
  628. mqttOnPublish(pid, [flag_ptr]() {
  629. (*flag_ptr) = true;
  630. });
  631. }
  632. #endif
  633. auto wait = res
  634. ? DiscoveryTask::WaitShortMs
  635. : DiscoveryTask::WaitLongMs;
  636. if (res || task.retry()) {
  637. schedule(wait, ptr, flag_ptr);
  638. return;
  639. }
  640. if (task.done()) {
  641. stop(true);
  642. return;
  643. }
  644. }
  645. } // namespace internal
  646. void publishDiscovery() {
  647. if (!mqttConnected() || internal::timer.active() || (internal::state != internal::State::Pending)) {
  648. return;
  649. }
  650. auto task = std::make_shared<DiscoveryTask>(internal::enabled);
  651. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  652. task->add<LightDiscovery>();
  653. #endif
  654. #if RELAY_SUPPORT
  655. task->add<RelayDiscovery>();
  656. #endif
  657. #if SENSOR_SUPPORT
  658. task->add<SensorDiscovery>();
  659. #endif
  660. // only happens when nothing is configured to do the add()
  661. if (task->done()) {
  662. return;
  663. }
  664. internal::schedule(task);
  665. }
  666. void configure() {
  667. bool current = internal::enabled;
  668. internal::enabled = getSetting("haEnabled", 1 == HOMEASSISTANT_ENABLED);
  669. internal::retain = getSetting("haRetain", 1 == HOMEASSISTANT_RETAIN);
  670. if (internal::enabled != current) {
  671. internal::state = internal::State::Pending;
  672. }
  673. homeassistant::publishDiscovery();
  674. }
  675. void mqttCallback(unsigned int type, const char* topic, char* payload) {
  676. if (MQTT_DISCONNECT_EVENT == type) {
  677. if (internal::state == internal::State::Sent) {
  678. internal::state = internal::State::Pending;
  679. }
  680. internal::timer.detach();
  681. return;
  682. }
  683. if (MQTT_CONNECT_EVENT == type) {
  684. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  685. ::mqttSubscribe(MQTT_TOPIC_LIGHT_JSON);
  686. schedule_function(publishLightJson);
  687. #endif
  688. schedule_function(publishDiscovery);
  689. return;
  690. }
  691. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  692. if (type == MQTT_MESSAGE_EVENT) {
  693. String t = ::mqttMagnitude(topic);
  694. if (t.equals(MQTT_TOPIC_LIGHT_JSON)) {
  695. receiveLightJson(payload);
  696. }
  697. return;
  698. }
  699. #endif
  700. }
  701. namespace web {
  702. #if WEB_SUPPORT
  703. void onVisible(JsonObject& root) {
  704. root["haVisible"] = 1;
  705. }
  706. void onConnected(JsonObject& root) {
  707. root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX);
  708. root["haEnabled"] = getSetting("haEnabled", 1 == HOMEASSISTANT_ENABLED);
  709. root["haRetain"] = getSetting("haRetain", 1 == HOMEASSISTANT_RETAIN);
  710. }
  711. bool onKeyCheck(const char* key, JsonVariant& value) {
  712. return (strncmp(key, "ha", 2) == 0);
  713. }
  714. #endif
  715. } // namespace web
  716. } // namespace homeassistant
  717. // This module does not implement .yaml generation, since we can't:
  718. // - use unique_id in the device config
  719. // - have abbreviated keys
  720. // - have mqtt return the correct status & command payloads when it is disabled
  721. // (yet? needs reworked configuration section or making functions read settings directly)
  722. void haSetup() {
  723. #if WEB_SUPPORT
  724. wsRegister()
  725. .onVisible(homeassistant::web::onVisible)
  726. .onConnected(homeassistant::web::onConnected)
  727. .onKeyCheck(homeassistant::web::onKeyCheck);
  728. #endif
  729. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  730. lightSetReportListener(homeassistant::publishLightJson);
  731. #endif
  732. mqttRegister(homeassistant::mqttCallback);
  733. #if TERMINAL_SUPPORT
  734. terminalRegisterCommand(F("HA.SEND"), [](const terminal::CommandContext& ctx) {
  735. using namespace homeassistant::internal;
  736. state = State::Pending;
  737. homeassistant::publishDiscovery();
  738. terminalOK(ctx);
  739. });
  740. #endif
  741. espurnaRegisterReload(homeassistant::configure);
  742. homeassistant::configure();
  743. }
  744. #endif // HOMEASSISTANT_SUPPORT