Official MQTT - Nuki Hub coexistence, keypad/timecontrol sensor per code/entry, option not to publish config, authorization log improvements, various fixes (#389)

* Coexistence with official MQTT over Wifi and Thread

* Coexistence with official MQTT over Wifi and Thread

* Arduino Core 2.0.17 cmake and README

* Coexistence with official MQTT over Wifi and Thread

* Keep updating status until state is known

* Coexistence with official MQTT over Wifi and Thread
This commit is contained in:
iranl
2024-06-08 09:03:35 +02:00
committed by GitHub
parent d2b3509d46
commit 90a8d04b45
19 changed files with 3647 additions and 1876 deletions

View File

@@ -10,7 +10,7 @@ RUN tar -xf /tmp/arduino-ide.tar.xz --directory ~/
RUN cd ~/arduino* && \
./install.sh && \
./arduino --pref "boardsmanager.additional.urls=https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json" --save-prefs && \
./arduino --install-boards esp32:esp32:2.0.15
./arduino --install-boards esp32:esp32:2.0.17
RUN git clone --recurse-submodules https://github.com/technyon/Arduino-CMake-Toolchain.git ~/Arduino-CMake-Toolchain
@@ -40,6 +40,6 @@ FROM builder AS runtime
COPY --from=builder /usr/src/nuki_hub/build/nuki_hub.bin /usr/src/nuki_hub/build/release/nuki_hub.bin
COPY --from=builder /usr/src/nuki_hub/build/nuki_hub.partitions.bin /usr/src/nuki_hub/build/release/nuki_hub.partitions.bin
COPY --from=builder /root/.arduino15/packages/esp32/hardware/esp32/2.0.15/tools/partitions/boot_app0.bin /usr/src/nuki_hub/build/release/boot_app0.bin
COPY --from=builder /root/.arduino15/packages/esp32/hardware/esp32/2.0.17/tools/partitions/boot_app0.bin /usr/src/nuki_hub/build/release/boot_app0.bin
CMD ["/bin/bash"]

50
HYBRID.md Normal file
View File

@@ -0,0 +1,50 @@
### Hybrid mode combining official MQTT implementation and Nuki Hub ###
The purpose of this mode is to have Nuki Hub work in conjunction with the official MQTT implementation by Nuki.
### Requirements ###
- ESP32 running Nuki Hub 8.35/8.36 or higher
- For WiFi: Nuki lock 3.0 Pro or Nuki Lock 4.0 Pro
- For Thread: Nuki Lock 4.0 or Nuki Lock 4.0 Pro. Note that you do ***NOT*** need to buy the remote access addon for the Nuki Lock 4.0
- For Thread: The Nuki Lock needs to have network access to the same MQTT server as the one that Nuki Hub is conected to. Depending if the MQTT server is reachable over IPv6 you might need an OpenThread Border router that supports NAT64 and has this enabled. Currently this means an Apple Device or Home Assistant with the Matter server and OpenThread Border Router
- The Nuki Opener does not have WiFI or Thread and thus doesn't benefit from the hybrid solutions added speed. You can however use and connect a Nuki Opener as usual which will function over regular BLE and can still connect Nuki Hub as a bridge to an Opener.
### Resources ###
- Discussion about setting up Nuki and Thread on the Nuki Developer forum: https://developer.nuki.io/t/smart-lock-4th-generation-set-up-home-assistant-for-remote-access-via-thread/25181
- Nuki KB article on MQTT: https://support.nuki.io/hc/en-us/articles/14052016143249-Activate-MQTT-via-the-Nuki-app
- Nuki KB article on Thread: https://support.nuki.io/hc/en-us/articles/18155425155217-Requirements-for-Remote-Access-via-Thread
### Reasons for using Hybrid mode combining official MQTT implementation and Nuki Hub
| Setup | Speed | Functionality | Battery life |
|---------------------------------------------|---------------|----------------|--------------|
| Nuki Hub paired as Bridge | -- | +++ | +++ |
| Nuki Hub paired as App | ++ | +++ | --- |
| Matter over Thread | +++ | - | ++ |
| Official MQTT over WiFi | +++ | + | - |
| Official MQTT over Thread | +++ | + | ++ |
| Hybrid Official MQTT over WiFi + Nuki Hub | +++ | +++ | -- |
| Hybrid Official MQTT over Thread + Nuki Hub | +++ | +++ | ++ |
The Hybrid Official MQTT over Thread + Nuki Hub solution allows for the best combination of state change speed, lock action execution, functionality and battery life.
### Setup ###
- Follow the official instruction by Nuki on setting up MQTT over Thread or WiFi on https://support.nuki.io/hc/en-us/articles/14052016143249-Activate-MQTT-via-the-Nuki-app.
- Make sure to connect to the same MQTT server as Nuki Hub
- Make sure ***NOT*** to enable `Auto discovery`, Home Assistant will be setup using Nuki Hub
- Optionally enable `Allow locking`. Note that if you enable this setting it is preferred to set ACL on your MQTT broker to only allow the Nuki lock and Nuki Hub MQTT user access to the topic `nuki/NUKI-ID/lockAction` to make sure that only Nuki Hub can execute commands on the lock (otherwise ACL settings through Nuki Hub can not be 100% enforced)
- Make sure that MQTT is setup correctly by checking if you get a green check mark in the Nuki app
- Install Nuki Hub 8.35 or higher on a supported ESP32 device
- Make sure you are not paired as a bridge. Unpair your Nuki lock in Nuki Hub if Nuki Hub was paired as a bridge (this is mandatory even if you removed the bridge connection from the Nuki lock).
- Enable `Enable hybrid official MQTT and Nuki Hub setup`. The `Lock: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)` setting will be automatically be enabled.
- Optionally enable `Enable sending actions through official MQTT`, if not enabled lock actions will be sent over BLE as usual (slower)
- Set `Time between status updates when official MQTT is offline (seconds)` to a positive integer. If the Nuki lock MQTT connection goes offline for whatever reason Nuki Hub will update the lock state with the set interval in seconds.
- Optionally enable `Retry command sent using official MQTT over BLE if failed`. If sending a lock action over the official MQTT implementation fails the command will be resent over BLE if this is enabled. Requires `Enable sending actions through official MQTT` to be enabled.
- Save your configuration
- Consider setting the `Query intervals` on the `Advanced Nuki configuration` to high numbers (e.g. 86400) to further reduce battery drain.
- Pair your Nuki Lock with Nuki Hub
- Test that state changes are recieved and processed by Nuki Hub by looking at the MQTT topics using an application like `MQTT Explorer`

View File

@@ -15,12 +15,12 @@ Feel free to join us on Discord: https://discord.gg/9nPq85bP4p
## Supported devices
<b>Supported ESP32 devices:</b>
- All ESP32 models with WIFI and BLE which are supported by Arduino Core 2.0.15 should work. Tested builds are provided for the ESP32, ESP32-S3 and ESP32-C3.
- All ESP32 models with WIFI and BLE which are supported by Arduino Core 2.0.17 should work. Tested builds are provided for the ESP32, ESP32-S3 and ESP32-C3.
- Untested builds are provided for the ESP32-Solo1.
<b>Not supported ESP32 devices:</b>
- The ESP32-S2 has no BLE and as such can't run Nuki Hub.
- The ESP32-C6 and ESP32-H2 are not supported by Arduino Core 2.0.15 and as such Nuki Hub is not compiled against these targets (at this time).
- The ESP32-C6 and ESP32-H2 are not supported by Arduino Core 2.0.17 and as such Nuki Hub is not compiled against these targets (at this time).
<b>Supported Nuki devices:</b>
- Nuki Smart Lock 1.0
@@ -81,9 +81,14 @@ When pairing is successful, the web interface should show "Paired: Yes" (it migh
MQTT nodes like lock state and battery level should now reflect the reported values from the lock.<br>
<br>
<b>Note: It is possible to run Nuki Hub alongside a Nuki Bridge.
This is not recommended and will lead to excessive battery drain and can lead to either device missing updates.
This is not recommended (unless when using [hybrid mode](/HYBRID.md)) and will lead to excessive battery drain and can lead to either device missing updates.
Enable "Register as app" before pairing to allow this. Otherwise the Bridge will be unregistered when pairing the Nuki Hub.</b>
## Hybrid mode
Hybrid mode allows you to use the official Nuki MQTT implemenation on a Nuki Lock 3.0 Pro, Nuki Lock 4.0 or Nuki Lock 4.0 Pro in conjunction with Nuki Hub.<br>
See [hybrid mode](/HYBRID.md) for more information.
## Support
If you haven't ordered your Nuki product yet, you can support me by using my referrer code when placing your order:<br>
@@ -124,6 +129,9 @@ In a browser navigate to the IP address assigned to the ESP32.
- Enable MQTT logging: Enable to fill the maintenance/log MQTT topic with debug log information.
- Check for Firmware Updates every 24h: Enable to allow the Nuki Hub to check the latest release of the Nuki Hub firmware on boot and every 24 hours. Requires the Nuki Hub to be able to connect to github.com. The latest version will be published to MQTT and will be visible on the main page of the Web Configurator.
- Disable some extraneous non-JSON topics: Enable to not publish non-JSON keypad and config MQTT topics.
- Enable hybrid official MQTT and Nuki Hub setup: Enable to combine the official MQTT over Thread/WiFi with BLE. Improves speed of state changes. Needs the official MQTT to be setup first. Also requires Nuki Hub to be paired as app and unregistered as a bridge using the Nuki app. See [hybrid mode](/HYBRID.md)
- Enable sending actions through official MQTT: Enable to sent lock actions through the official MQTT topics (e.g. over Thread/WiFi) instead of using BLE. Needs "Enable hybrid official MQTT and Nuki Hub setup" to be enabled. See [hybrid mode](/HYBRID.md)
- Time between status updates when official MQTT is offline (seconds): Set to a positive integer to set the maximum amount of seconds between actively querying the Nuki lock for the current lock state when the official MQTT is offline, default 600.
#### IP Address assignment
@@ -150,13 +158,15 @@ In a browser navigate to the IP address assigned to the ESP32.
- Query interval keypad (Only available when a Keypad is detected): Set to a positive integer to set the maximum amount of seconds between actively querying the Nuki device for the current keypad state, default 1800.
- Number of retries if command failed: Set to a positive integer to define the amount of times the Nuki Hub retries sending commands to the Nuki Lock or Opener when commands are not acknowledged by the device, default 3.
- Delay between retries: Set to the amount of milliseconds the Nuki Hub waits between resending not acknowledged commands, default 100.
- Nuki Bridge is running alongside Nuki Hub: Enable to allow Nuki Hub to co-exist with a Nuki Bridge by registering Nuki Hub as an (smartphone) app instead of a bridge. Changing this setting will require re-pairing. Enabling this setting is strongly discouraged as described in the "[Pairing with a Nuki Lock or Opener](#pairing-with-a-nuki-lock-or-opener)" section of this README
- Lock: Nuki Bridge is running alongside Nuki Hub: Enable to allow Nuki Hub to co-exist with a Nuki Bridge by registering Nuki Hub as an (smartphone) app instead of a bridge. Changing this setting will require re-pairing. Enabling this setting is strongly discouraged as described in the "[Pairing with a Nuki Lock or Opener](#pairing-with-a-nuki-lock-or-opener)" section of this README, ***unless when used in [Hybrid mode](/HYBRID.md)e (Official MQTT / Nuki Hub co-existance)***
- Opener: Nuki Bridge is running alongside Nuki Hub: Enable to allow Nuki Hub to co-exist with a Nuki Bridge by registering Nuki Hub as an (smartphone) app instead of a bridge. Changing this setting will require re-pairing. Enabling this setting is strongly discouraged as described in the "[Pairing with a Nuki Lock or Opener](#pairing-with-a-nuki-lock-or-opener)" section of this README
- Presence detection timeout: Set to a positive integer to set the amount of seconds between updates to the presence/devices MQTT topic with the list of detected bluetooth devices, set to -1 to disable presence detection, default 60.
- Restart if bluetooth beacons not received: Set to a positive integer to restart the Nuki Hub after the set amount of seconds has passed without receiving a bluetooth beacon from the Nuki device, set to -1 to disable, default 60. Because the bluetooth stack of the ESP32 can silently fail it is not recommended to disable this setting.
### Access Level Configuration
#### Nuki General Access Control
- Publish Nuki configuration information: Enable to publish information about the configuration of the connected Nuki device(s) through MQTT.
- Publish keypad entries information (Only available when a Keypad is detected): Enable to publish information about keypad codes through MQTT, see the "[Keypad control](#keypad-control-optional)" section of this README
- Also publish keypad codes (Only available when a Keypad is detected): Enable to publish the actual keypad codes through MQTT, note that is could be considered a security risk
- Add, modify and delete keypad codes (Only available when a Keypad is detected): Enable to allow configuration of keypad codes through MQTT, see the "[Keypad control](#keypad-control-optional)" section of this README
@@ -271,6 +281,7 @@ In a browser navigate to the IP address assigned to the ESP32.
- battery/maxTurnCurrent: The highest current of the turn motor during the last lock action (A) (Lock only).
- battery/lockDistance: The total distance during the last lock action in centidegrees (Lock only).
- battery/keypadCritical: 1 if the battery level of a connected keypad is critical, otherwise 0.
- battery/doorSensorCritical (only available in hybdrid mode): 1 if the battery level of a connected doorsensor is critical, otherwise 0.
- battery/basicJson: The current battery state (critical, charging, level and keypad critical) of the Nuki Lock/Opener as JSON data.
- battery/advancedJson: : The current battery state (critical, batteryDrain, batteryVoltage, lockAction, startVoltage, lowestVoltage, lockDistance, startTemperature, maxTurnCurrent and batteryResistance) of the Nuki Lock/Opener as JSON data.
@@ -478,9 +489,9 @@ To change Nuki Lock/Opener keypad settings set the `keypad/actionJson` topic to
| action | Required | Required | Required | The action to execute | "delete", "add", "update" |
| codeId | Required | Not used | Required | The code ID of the existing code to delete or update | Integer |
| code | Not used | Required | Optional | The code to create or update | 6-digit Integer without zero's, can't start with "12"|
| enabled | Not used | Not used | Optional | Enable or disable the code, always enabled on add, disabled if not set on update | 1 = enabled, 0 = disabled |
| name | Not used | Required | Required | The name of the code to create or update | String, max 20 chars |
| timeLimited | Not used | Optional | Optional | If this authorization is restricted to access only at certain times, disabled if not set (requires enabled = 1) | 1 = enabled, 0 = disabled |
| enabled | Not used | Not used | Optional | Enable or disable the code, always enabled on add | 1 = enabled, 0 = disabled |
| name | Not used | Required | Optional | The name of the code to create or update | String, max 20 chars |
| timeLimited | Not used | Optional | Optional | If this authorization is restricted to access only at certain times, requires enabled = 1 | 1 = enabled, 0 = disabled |
| allowedFrom | Not used | Optional | Optional | The start timestamp from which access should be allowed (requires enabled = 1 and timeLimited = 1) | "YYYY-MM-DD HH:MM:SS" |
| allowedUntil | Not used | Optional | Optional | The end timestamp until access should be allowed (requires enabled = 1 and timeLimited = 1) | "YYYY-MM-DD HH:MM:SS" |
| allowedWeekdays | Not used | Optional | Optional | Weekdays on which access should be allowed (requires enabled = 1 and timeLimited = 1) | Array of days: "mon", "tue", "wed", "thu" , "fri" "sat", "sun"|
@@ -682,11 +693,11 @@ Navigate to "Nuki Configuration" and change the "MQTT Nuki Smartlock Path" or "M
### The Nuki battery is draining quickly.
This often is a result of enabling "Register as app".<br>
This often is a result of enabling "Register as app" when not using [Hybrid mode](/HYBRID.md) (Official MQTT / Nuki Hub co-existance).<br>
Doing so will cause Nuki Hub to constantly query the lock and as such cause excessive battery drain.<br>
To prevent this behaviour, unpair Nuki Hub, disable "Register as app", and re-pair.<br>
<br>
<b>Never enable "Register as app" unless you intend to use a Nuki Bridge in addition to Nuki Hub!</b>
<b>Never enable "Register as app" unless you intend to use a Nuki Bridge in addition to Nuki Hub or you are using Hybrid mode!</b>
## Building from source
<b>Docker (Preferred)</b><br>

View File

@@ -24,6 +24,19 @@
#define mqtt_topic_lock_address "/lock/address"
#define mqtt_topic_lock_retry "/lock/retry"
#define mqtt_topic_official_lock_action "/lockAction"
//#define mqtt_topic_official_mode "/mode"
#define mqtt_topic_official_state "/state"
#define mqtt_topic_official_batteryCritical "/batteryCritical"
#define mqtt_topic_official_batteryChargeState "/batteryChargeState"
#define mqtt_topic_official_batteryCharging "/batteryCharging"
#define mqtt_topic_official_keypadBatteryCritical "/keypadBatteryCritical"
#define mqtt_topic_official_doorsensorState "/doorsensorState"
#define mqtt_topic_official_doorsensorBatteryCritical "/doorsensorBatteryCritical"
#define mqtt_topic_official_connected "/connected"
#define mqtt_topic_official_commandResponse "/commandResponse"
#define mqtt_topic_official_lockActionEvent "/lockActionEvent"
#define mqtt_topic_config_action "/configuration/action"
#define mqtt_topic_config_action_command_result "/configuration/commandResult"
#define mqtt_topic_config_basic_json "/configuration/basicJson"
@@ -50,10 +63,12 @@
#define mqtt_topic_battery_max_turn_current "/battery/maxTurnCurrent"
#define mqtt_topic_battery_lock_distance "/battery/lockDistance"
#define mqtt_topic_battery_keypad_critical "/battery/keypadCritical"
#define mqtt_topic_battery_doorsensor_critical "/battery/doorSensorCritical"
#define mqtt_topic_battery_basic_json "/battery/basicJson"
#define mqtt_topic_battery_advanced_json "/battery/advancedJson"
#define mqtt_topic_keypad "/keypad"
#define mqtt_topic_keypad_codes "/keypad/codes"
#define mqtt_topic_keypad_command_action "/keypad/command/action"
#define mqtt_topic_keypad_command_id "/keypad/command/id"
#define mqtt_topic_keypad_command_name "/keypad/command/name"
@@ -64,6 +79,8 @@
#define mqtt_topic_keypad_json_action "/keypad/actionJson"
#define mqtt_topic_keypad_json_command_result "/keypad/commandResultJson"
#define mqtt_topic_timecontrol "/timecontrol"
#define mqtt_topic_timecontrol_entries "/timecontrol/entries"
#define mqtt_topic_timecontrol_json "/timecontrol/json"
#define mqtt_topic_timecontrol_action "/timecontrol/action"
#define mqtt_topic_timecontrol_command_result "/timecontrol/commandResult"

View File

@@ -160,7 +160,7 @@ void Network::setupDevice()
void Network::initialize()
{
_restartOnDisconnect = _preferences->getBool(preference_restart_on_disconnect);
_restartOnDisconnect = _preferences->getBool(preference_restart_on_disconnect, false);
_rssiPublishInterval = _preferences->getInt(preference_rssi_publish_interval) * 1000;
_hostname = _preferences->getString(preference_hostname);
@@ -358,6 +358,8 @@ bool Network::update()
if(_lastMaintenanceTs == 0 || (ts - _lastMaintenanceTs) > 30000)
{
publishULong(_maintenancePathPrefix, mqtt_topic_uptime, ts / 1000 / 60);
publishString(_maintenancePathPrefix, mqtt_topic_mqtt_connection_state, "online");
if(_publishDebugInfo)
{
publishUInt(_maintenancePathPrefix, mqtt_topic_freeheap, esp_get_free_heap_size());
@@ -706,56 +708,56 @@ const String Network::networkBSSID() const
return _device->BSSIDstr();
}
void Network::publishFloat(const char* prefix, const char* topic, const float value, const uint8_t precision)
void Network::publishFloat(const char* prefix, const char* topic, const float value, const uint8_t precision, bool retain)
{
char str[30];
dtostrf(value, 0, precision, str);
char path[200] = {0};
buildMqttPath(path, { prefix, topic });
_device->mqttPublish(path, MQTT_QOS_LEVEL, true, str);
_device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str);
}
void Network::publishInt(const char* prefix, const char *topic, const int value)
void Network::publishInt(const char* prefix, const char *topic, const int value, bool retain)
{
char str[30];
itoa(value, str, 10);
char path[200] = {0};
buildMqttPath(path, { prefix, topic });
_device->mqttPublish(path, MQTT_QOS_LEVEL, true, str);
_device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str);
}
void Network::publishUInt(const char* prefix, const char *topic, const unsigned int value)
void Network::publishUInt(const char* prefix, const char *topic, const unsigned int value, bool retain)
{
char str[30];
utoa(value, str, 10);
char path[200] = {0};
buildMqttPath(path, { prefix, topic });
_device->mqttPublish(path, MQTT_QOS_LEVEL, true, str);
_device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str);
}
void Network::publishULong(const char* prefix, const char *topic, const unsigned long value)
void Network::publishULong(const char* prefix, const char *topic, const unsigned long value, bool retain)
{
char str[30];
utoa(value, str, 10);
char path[200] = {0};
buildMqttPath(path, { prefix, topic });
_device->mqttPublish(path, MQTT_QOS_LEVEL, true, str);
_device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str);
}
void Network::publishBool(const char* prefix, const char *topic, const bool value)
void Network::publishBool(const char* prefix, const char *topic, const bool value, bool retain)
{
char str[2] = {0};
str[0] = value ? '1' : '0';
char path[200] = {0};
buildMqttPath(path, { prefix, topic });
_device->mqttPublish(path, MQTT_QOS_LEVEL, true, str);
_device->mqttPublish(path, MQTT_QOS_LEVEL, retain, str);
}
bool Network::publishString(const char* prefix, const char *topic, const char *value)
bool Network::publishString(const char* prefix, const char *topic, const char *value, bool retain)
{
char path[200] = {0};
buildMqttPath(path, { prefix, topic });
return _device->mqttPublish(path, MQTT_QOS_LEVEL, true, value) > 0;
return _device->mqttPublish(path, MQTT_QOS_LEVEL, retain, value) > 0;
}
void Network::publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, const char *softwareVersion, const char *hardwareVersion, const char* availabilityTopic, const bool& hasKeypad, char* lockAction, char* unlockAction, char* openAction)
@@ -796,6 +798,8 @@ void Network::publishHASSConfig(char* deviceType, const char* baseTopic, char* n
json["stat_locking"] = "locking";
json["stat_unlocked"] = "unlocked";
json["stat_unlocking"] = "unlocking";
json["stat_open"] = "open";
json["stat_opening"] = "opening";
json["opt"] = "false";
serializeJson(json, _buffer, _bufferSize);
@@ -1101,10 +1105,15 @@ void Network::publishHASSConfigAdditionalLockEntities(char *deviceType, const ch
{
uint32_t aclPrefs[17];
_preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs));
uint32_t basicLockConfigAclPrefs[16];
_preferences->getBytes(preference_conf_lock_basic_acl, &basicLockConfigAclPrefs, sizeof(basicLockConfigAclPrefs));
uint32_t advancedLockConfigAclPrefs[22];
_preferences->getBytes(preference_conf_lock_advanced_acl, &advancedLockConfigAclPrefs, sizeof(advancedLockConfigAclPrefs));
uint32_t basicLockConfigAclPrefs[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
uint32_t advancedLockConfigAclPrefs[22] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
if(_preferences->getBool(preference_conf_info_enabled, true))
{
_preferences->getBytes(preference_conf_lock_basic_acl, &basicLockConfigAclPrefs, sizeof(basicLockConfigAclPrefs));
_preferences->getBytes(preference_conf_lock_advanced_acl, &advancedLockConfigAclPrefs, sizeof(advancedLockConfigAclPrefs));
}
if((int)aclPrefs[2])
{
@@ -2166,10 +2175,14 @@ void Network::publishHASSConfigAdditionalOpenerEntities(char *deviceType, const
{
uint32_t aclPrefs[17];
_preferences->getBytes(preference_acl, &aclPrefs, sizeof(aclPrefs));
uint32_t basicOpenerConfigAclPrefs[16];
_preferences->getBytes(preference_conf_opener_basic_acl, &basicOpenerConfigAclPrefs, sizeof(basicOpenerConfigAclPrefs));
uint32_t advancedOpenerConfigAclPrefs[22];
_preferences->getBytes(preference_conf_opener_advanced_acl, &advancedOpenerConfigAclPrefs, sizeof(advancedOpenerConfigAclPrefs));
uint32_t basicOpenerConfigAclPrefs[14] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
uint32_t advancedOpenerConfigAclPrefs[20] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
if(_preferences->getBool(preference_conf_info_enabled, true))
{
_preferences->getBytes(preference_conf_opener_basic_acl, &basicOpenerConfigAclPrefs, sizeof(basicOpenerConfigAclPrefs));
_preferences->getBytes(preference_conf_opener_advanced_acl, &advancedOpenerConfigAclPrefs, sizeof(advancedOpenerConfigAclPrefs));
}
if((int)aclPrefs[11])
{
@@ -2195,64 +2208,6 @@ void Network::publishHASSConfigAdditionalOpenerEntities(char *deviceType, const
removeHassTopic((char*)"button", (char*)"unlatch", uidString);
}
if((int)basicOpenerConfigAclPrefs[5] == 1)
{
// LED enabled
publishHassTopic("switch",
"led_enabled",
uidString,
"_led_enabled",
"LED enabled",
name,
baseTopic,
String("~") + mqtt_topic_config_basic_json,
deviceType,
"",
"",
"config",
String("~") + mqtt_topic_config_action,
{ { (char*)"en", (char*)"true" },
{ (char*)"ic", (char*)"mdi:led-variant-on" },
{ (char*)"pl_on", (char*)"{ \"ledEnabled\": \"1\"}" },
{ (char*)"pl_off", (char*)"{ \"ledEnabled\": \"0\"}" },
{ (char*)"val_tpl", (char*)"{{value_json.ledEnabled}}" },
{ (char*)"stat_on", (char*)"1" },
{ (char*)"stat_off", (char*)"0" }});
}
else
{
removeHassTopic((char*)"switch", (char*)"led_enabled", uidString);
}
if((int)basicOpenerConfigAclPrefs[4] == 1)
{
// Button enabled
publishHassTopic("switch",
"button_enabled",
uidString,
"_button_enabled",
"Button enabled",
name,
baseTopic,
String("~") + mqtt_topic_config_basic_json,
deviceType,
"",
"",
"config",
String("~") + mqtt_topic_config_action,
{ { (char*)"en", (char*)"true" },
{ (char*)"ic", (char*)"mdi:radiobox-marked" },
{ (char*)"pl_on", (char*)"{ \"buttonEnabled\": \"1\"}" },
{ (char*)"pl_off", (char*)"{ \"buttonEnabled\": \"0\"}" },
{ (char*)"val_tpl", (char*)"{{value_json.buttonEnabled}}" },
{ (char*)"stat_on", (char*)"1" },
{ (char*)"stat_off", (char*)"0" }});
}
else
{
removeHassTopic((char*)"switch", (char*)"button_enabled", uidString);
}
publishHassTopic("binary_sensor",
"continuous_mode",
uidString,
@@ -2319,6 +2274,64 @@ void Network::publishHASSConfigAdditionalOpenerEntities(char *deviceType, const
String path = createHassTopicPath("event", "ring", uidString);
_device->mqttPublish(path.c_str(), MQTT_QOS_LEVEL, true, _buffer);
if((int)basicOpenerConfigAclPrefs[5] == 1)
{
// LED enabled
publishHassTopic("switch",
"led_enabled",
uidString,
"_led_enabled",
"LED enabled",
name,
baseTopic,
String("~") + mqtt_topic_config_basic_json,
deviceType,
"",
"",
"config",
String("~") + mqtt_topic_config_action,
{ { (char*)"en", (char*)"true" },
{ (char*)"ic", (char*)"mdi:led-variant-on" },
{ (char*)"pl_on", (char*)"{ \"ledEnabled\": \"1\"}" },
{ (char*)"pl_off", (char*)"{ \"ledEnabled\": \"0\"}" },
{ (char*)"val_tpl", (char*)"{{value_json.ledEnabled}}" },
{ (char*)"stat_on", (char*)"1" },
{ (char*)"stat_off", (char*)"0" }});
}
else
{
removeHassTopic((char*)"switch", (char*)"led_enabled", uidString);
}
if((int)basicOpenerConfigAclPrefs[4] == 1)
{
// Button enabled
publishHassTopic("switch",
"button_enabled",
uidString,
"_button_enabled",
"Button enabled",
name,
baseTopic,
String("~") + mqtt_topic_config_basic_json,
deviceType,
"",
"",
"config",
String("~") + mqtt_topic_config_action,
{ { (char*)"en", (char*)"true" },
{ (char*)"ic", (char*)"mdi:radiobox-marked" },
{ (char*)"pl_on", (char*)"{ \"buttonEnabled\": \"1\"}" },
{ (char*)"pl_off", (char*)"{ \"buttonEnabled\": \"0\"}" },
{ (char*)"val_tpl", (char*)"{{value_json.buttonEnabled}}" },
{ (char*)"stat_on", (char*)"1" },
{ (char*)"stat_off", (char*)"0" }});
}
else
{
removeHassTopic((char*)"switch", (char*)"button_enabled", uidString);
}
if((int)advancedOpenerConfigAclPrefs[15] == 1)
{
publishHassTopic("number",

View File

@@ -40,12 +40,12 @@ public:
void subscribe(const char* prefix, const char* path);
void initTopic(const char* prefix, const char* path, const char* value);
void publishFloat(const char* prefix, const char* topic, const float value, const uint8_t precision = 2);
void publishInt(const char* prefix, const char* topic, const int value);
void publishUInt(const char* prefix, const char* topic, const unsigned int value);
void publishULong(const char* prefix, const char* topic, const unsigned long value);
void publishBool(const char* prefix, const char* topic, const bool value);
bool publishString(const char* prefix, const char* topic, const char* value);
void publishFloat(const char* prefix, const char* topic, const float value, const uint8_t precision = 2, bool retain = false);
void publishInt(const char* prefix, const char* topic, const int value, bool retain = false);
void publishUInt(const char* prefix, const char* topic, const unsigned int value, bool retain = false);
void publishULong(const char* prefix, const char* topic, const unsigned long value, bool retain = false);
void publishBool(const char* prefix, const char* topic, const bool value, bool retain = false);
bool publishString(const char* prefix, const char* topic, const char* value, bool retain = false);
void publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, const char *softwareVersion, const char *hardwareVersion, const char* availabilityTopic, const bool& hasKeypad, char* lockAction, char* unlockAction, char* openAction);
void publishHASSConfigAdditionalLockEntities(char* deviceType, const char* baseTopic, char* name, char* uidString);
@@ -56,6 +56,22 @@ public:
void publishHASSWifiRssiConfig(char* deviceType, const char* baseTopic, char* name, char* uidString);
void removeHASSConfig(char* uidString);
void removeHASSConfigTopic(char* deviceType, char* name, char* uidString);
void publishHassTopic(const String& mqttDeviceType,
const String& mqttDeviceName,
const String& uidString,
const String& uidStringPostfix,
const String& displayName,
const String& name,
const String& baseTopic,
const String& stateTopic,
const String& deviceType,
const String& deviceClass,
const String& stateClass = "",
const String& entityCat = "",
const String& commandTopic = "",
std::vector<std::pair<char*, char*>> additionalEntries = {}
);
void removeHassTopic(const String& mqttDeviceType, const String& mqttDeviceName, const String& uidString);
void removeTopic(const String& mqttPath, const String& mqttTopic);
void batteryTypeToString(const Nuki::BatteryType battype, char* str);
void advertisingModeToString(const Nuki::AdvertisingMode advmode, char* str);
@@ -87,24 +103,7 @@ private:
void setupDevice();
bool reconnect();
void publishHassTopic(const String& mqttDeviceType,
const String& mqttDeviceName,
const String& uidString,
const String& uidStringPostfix,
const String& displayName,
const String& name,
const String& baseTopic,
const String& stateTopic,
const String& deviceType,
const String& deviceClass,
const String& stateClass = "",
const String& entityCat = "",
const String& commandTopic = "",
std::vector<std::pair<char*, char*>> additionalEntries = {}
);
String createHassTopicPath(const String& mqttDeviceType, const String& mqttDeviceName, const String& uidString);
void removeHassTopic(const String& mqttDeviceType, const String& mqttDeviceName, const String& uidString);
JsonDocument createHassJson(const String& uidString,
const String& uidStringPostfix,
const String& displayName,

View File

@@ -6,6 +6,7 @@
#include "Logger.h"
#include "RestartReason.h"
#include <ArduinoJson.h>
#include <ctype.h>
NetworkLock::NetworkLock(Network* network, Preferences* preferences, char* buffer, size_t bufferSize)
: _network(network),
@@ -16,6 +17,19 @@ NetworkLock::NetworkLock(Network* network, Preferences* preferences, char* buffe
memset(_authName, 0, sizeof(_authName));
_authName[0] = '\0';
_offTopics.reserve(10);
//_offTopics.push_back(mqtt_topic_official_mode);
_offTopics.push_back((char*)mqtt_topic_official_state);
_offTopics.push_back((char*)mqtt_topic_official_batteryCritical);
_offTopics.push_back((char*)mqtt_topic_official_batteryChargeState);
_offTopics.push_back((char*)mqtt_topic_official_batteryCharging);
_offTopics.push_back((char*)mqtt_topic_official_keypadBatteryCritical);
_offTopics.push_back((char*)mqtt_topic_official_doorsensorState);
_offTopics.push_back((char*)mqtt_topic_official_doorsensorBatteryCritical);
_offTopics.push_back((char*)mqtt_topic_official_connected);
_offTopics.push_back((char*)mqtt_topic_official_commandResponse);
_offTopics.push_back((char*)mqtt_topic_official_lockActionEvent);
_network->registerMqttReceiver(this);
}
@@ -88,6 +102,18 @@ void NetworkLock::initialize()
//_network->removeTopic(_mqttPath, mqtt_topic_presence);
}
if(!_preferences->getBool(preference_conf_info_enabled, false))
{
_network->removeTopic(_mqttPath, mqtt_topic_config_basic_json);
_network->removeTopic(_mqttPath, mqtt_topic_config_advanced_json);
_network->removeTopic(_mqttPath, mqtt_topic_config_button_enabled);
_network->removeTopic(_mqttPath, mqtt_topic_config_led_enabled);
_network->removeTopic(_mqttPath, mqtt_topic_config_led_brightness);
_network->removeTopic(_mqttPath, mqtt_topic_config_auto_unlock);
_network->removeTopic(_mqttPath, mqtt_topic_config_auto_lock);
_network->removeTopic(_mqttPath, mqtt_topic_config_single_lock);
}
if(_preferences->getBool(preference_keypad_control_enabled))
{
if(!_preferences->getBool(preference_disable_non_json, false))
@@ -116,6 +142,20 @@ void NetworkLock::initialize()
_network->initTopic(_mqttPath, mqtt_topic_timecontrol_action, "--");
}
if(_preferences->getBool(preference_official_hybrid, false))
{
char uidString[20];
itoa(_preferences->getUInt(preference_nuki_id_lock, 0), uidString, 16);
for(char* c=uidString; *c=toupper(*c); ++c);
strcpy(_offMqttPath, "nuki/");
strcat(_offMqttPath,uidString);
for(const auto& offTopic : _offTopics)
{
_network->subscribe(_offMqttPath, offTopic);
}
}
if(_preferences->getBool(preference_publish_authdata, false))
{
_network->subscribe(_mqttPath, mqtt_topic_lock_log_rolling_last);
@@ -170,6 +210,20 @@ void NetworkLock::onMqttDataReceived(const char* topic, byte* payload, const uns
if(atoi(value) > 0 && atoi(value) > _lastRollingLog) _lastRollingLog = atoi(value);
}
if(_preferences->getBool(preference_official_hybrid, false))
{
for(auto offTopic : _offTopics)
{
if(comparePrefixedPath(topic, offTopic, true))
{
if(_officialUpdateReceivedCallback != nullptr)
{
_officialUpdateReceivedCallback(offTopic, value);
}
}
}
}
if(comparePrefixedPath(topic, mqtt_topic_lock_action))
{
if(strcmp(value, "") == 0 ||
@@ -313,31 +367,49 @@ void NetworkLock::publishKeyTurnerState(const NukiLock::KeyTurnerState& keyTurne
JsonDocument json;
JsonDocument jsonBattery;
lockstateToString(keyTurnerState.lockState, str);
if(keyTurnerState.lockState != NukiLock::LockState::Undefined)
if(!_offConnected)
{
lockstateToString(keyTurnerState.lockState, str);
publishString(mqtt_topic_lock_state, str);
if(_haEnabled)
if(keyTurnerState.lockState != NukiLock::LockState::Undefined)
{
publishState(keyTurnerState.lockState);
publishString(mqtt_topic_lock_state, str);
if(_haEnabled)
{
publishState(keyTurnerState.lockState);
}
}
json["lock_state"] = str;
}
else
{
lockstateToString((NukiLock::LockState)_offState, str);
json["lock_state"] = str;
}
json["lock_state"] = str;
json["lockngo_state"] = (keyTurnerState.lockNgoTimer == 0 ? 0 : 1);
memset(&str, 0, sizeof(str));
triggerToString(keyTurnerState.trigger, str);
if(_firstTunerStatePublish || keyTurnerState.trigger != lastKeyTurnerState.trigger)
if(!_offConnected)
{
publishString(mqtt_topic_lock_trigger, str);
}
triggerToString(keyTurnerState.trigger, str);
json["trigger"] = str;
if(_firstTunerStatePublish || keyTurnerState.trigger != lastKeyTurnerState.trigger)
{
publishString(mqtt_topic_lock_trigger, str);
}
json["trigger"] = str;
}
else
{
triggerToString((NukiLock::Trigger)_offTrigger, str);
json["trigger"] = str;
}
char curTime[20];
sprintf(curTime, "%04d-%02d-%02d %02d:%02d:%02d", keyTurnerState.currentTimeYear, keyTurnerState.currentTimeMonth, keyTurnerState.currentTimeDay, keyTurnerState.currentTimeHour, keyTurnerState.currentTimeMinute, keyTurnerState.currentTimeSecond);
@@ -346,14 +418,23 @@ void NetworkLock::publishKeyTurnerState(const NukiLock::KeyTurnerState& keyTurne
json["nightModeActive"] = keyTurnerState.nightModeActive;
memset(&str, 0, sizeof(str));
lockactionToString(keyTurnerState.lastLockAction, str);
if(_firstTunerStatePublish || keyTurnerState.lastLockAction != lastKeyTurnerState.lastLockAction)
if(!_offConnected)
{
publishString(mqtt_topic_lock_last_lock_action, str);
}
lockactionToString(keyTurnerState.lastLockAction, str);
json["last_lock_action"] = str;
if(_firstTunerStatePublish || keyTurnerState.lastLockAction != lastKeyTurnerState.lastLockAction)
{
publishString(mqtt_topic_lock_last_lock_action, str);
}
json["last_lock_action"] = str;
}
else
{
lockactionToString((NukiLock::LockAction)_offLockAction, str);
json["last_lock_action"] = str;
}
memset(&str, 0, sizeof(str));
triggerToString(keyTurnerState.lastLockActionTrigger, str);
@@ -368,37 +449,45 @@ void NetworkLock::publishKeyTurnerState(const NukiLock::KeyTurnerState& keyTurne
}
json["lock_completion_status"] = str;
memset(&str, 0, sizeof(str));
NukiLock::doorSensorStateToString(keyTurnerState.doorSensorState, str);
if(_firstTunerStatePublish || keyTurnerState.doorSensorState != lastKeyTurnerState.doorSensorState)
if(!_offConnected)
{
publishString(mqtt_topic_lock_door_sensor_state, str);
NukiLock::doorSensorStateToString(keyTurnerState.doorSensorState, str);
if(_firstTunerStatePublish || keyTurnerState.doorSensorState != lastKeyTurnerState.doorSensorState)
{
publishString(mqtt_topic_lock_door_sensor_state, str);
}
json["door_sensor_state"] = str;
bool critical = (keyTurnerState.criticalBatteryState & 0b00000001) > 0;
bool charging = (keyTurnerState.criticalBatteryState & 0b00000010) > 0;
uint8_t level = (keyTurnerState.criticalBatteryState & 0b11111100) >> 1;
bool keypadCritical = (keyTurnerState.accessoryBatteryState & (1 << 7)) != 0 ? (keyTurnerState.accessoryBatteryState & (1 << 6)) != 0 : false;
jsonBattery["critical"] = critical ? "1" : "0";
jsonBattery["charging"] = charging ? "1" : "0";
jsonBattery["level"] = level;
jsonBattery["keypadCritical"] = keypadCritical ? "1" : "0";
if((_firstTunerStatePublish || keyTurnerState.criticalBatteryState != lastKeyTurnerState.criticalBatteryState) && !_preferences->getBool(preference_disable_non_json, false))
{
publishBool(mqtt_topic_battery_critical, critical);
publishBool(mqtt_topic_battery_charging, charging);
publishInt(mqtt_topic_battery_level, level);
}
if((_firstTunerStatePublish || keyTurnerState.accessoryBatteryState != lastKeyTurnerState.accessoryBatteryState) && !_preferences->getBool(preference_disable_non_json, false))
{
publishBool(mqtt_topic_battery_keypad_critical, keypadCritical);
}
}
json["door_sensor_state"] = str;
bool critical = (keyTurnerState.criticalBatteryState & 0b00000001) > 0;
bool charging = (keyTurnerState.criticalBatteryState & 0b00000010) > 0;
uint8_t level = (keyTurnerState.criticalBatteryState & 0b11111100) >> 1;
bool keypadCritical = (keyTurnerState.accessoryBatteryState & (1 << 7)) != 0 ? (keyTurnerState.accessoryBatteryState & (1 << 6)) != 0 : false;
jsonBattery["critical"] = critical ? "1" : "0";
jsonBattery["charging"] = charging ? "1" : "0";
jsonBattery["level"] = level;
jsonBattery["keypadCritical"] = keypadCritical ? "1" : "0";
if((_firstTunerStatePublish || keyTurnerState.criticalBatteryState != lastKeyTurnerState.criticalBatteryState) && !_preferences->getBool(preference_disable_non_json, false))
else
{
publishBool(mqtt_topic_battery_critical, critical);
publishBool(mqtt_topic_battery_charging, charging);
publishInt(mqtt_topic_battery_level, level);
}
if((_firstTunerStatePublish || keyTurnerState.accessoryBatteryState != lastKeyTurnerState.accessoryBatteryState) && !_preferences->getBool(preference_disable_non_json, false))
{
publishBool(mqtt_topic_battery_keypad_critical, keypadCritical);
NukiLock::doorSensorStateToString((NukiLock::DoorSensorState)_offDoorsensorState, str);
json["door_sensor_state"] = str;
}
json["auth_id"] = _authId;
@@ -430,12 +519,18 @@ void NetworkLock::publishState(NukiLock::LockState lockState)
publishString(mqtt_topic_lock_binary_state, "unlocked");
break;
case NukiLock::LockState::Unlocked:
case NukiLock::LockState::Unlatched:
case NukiLock::LockState::Unlatching:
case NukiLock::LockState::UnlockedLnga:
publishString(mqtt_topic_lock_ha_state, "unlocked");
publishString(mqtt_topic_lock_binary_state, "unlocked");
break;
case NukiLock::LockState::Unlatched:
publishString(mqtt_topic_lock_ha_state, "open");
publishString(mqtt_topic_lock_binary_state, "unlocked");
break;
case NukiLock::LockState::Unlatching:
publishString(mqtt_topic_lock_ha_state, "opening");
publishString(mqtt_topic_lock_binary_state, "unlocked");
break;
case NukiLock::LockState::Uncalibrated:
case NukiLock::LockState::Calibration:
case NukiLock::LockState::BootRun:
@@ -506,22 +601,41 @@ void NetworkLock::publishAuthorizationInfo(const std::list<NukiLock::LogEntry>&
memset(str, 0, sizeof(str));
NukiLock::completionStatusToString((NukiLock::CompletionStatus)log.data[3], str);
entry["completionStatus"] = str;
entry["completionStatusVal"] = log.data[3];
break;
case NukiLock::LoggingType::KeypadAction:
memset(str, 0, sizeof(str));
NukiLock::lockactionToString((NukiLock::LockAction)log.data[0], str);
entry["action"] = str;
switch(log.data[1])
{
case 0:
entry["trigger"] = "arrowkey";
break;
case 1:
entry["trigger"] = "code";
break;
case 2:
entry["trigger"] = "fingerprint";
break;
default:
entry["trigger"] = "Unknown";
break;
}
memset(str, 0, sizeof(str));
NukiLock::completionStatusToString((NukiLock::CompletionStatus)log.data[2], str);
entry["completionStatus"] = str;
entry["completionStatusVal"] = log.data[2];
if(log.data[2] == 9) entry["completionStatus"] = "notAuthorized";
else if (log.data[2] == 224) entry["completionStatus"] = "invalidCode";
else
{
NukiLock::completionStatusToString((NukiLock::CompletionStatus)log.data[2], str);
entry["completionStatus"] = str;
}
entry["codeId"] = 256U*log.data[4]+log.data[3];
break;
case NukiLock::LoggingType::DoorSensor:
memset(str, 0, sizeof(str));
NukiLock::lockactionToString((NukiLock::LockAction)log.data[0], str);
switch(log.data[0])
{
case 0:
@@ -537,10 +651,6 @@ void NetworkLock::publishAuthorizationInfo(const std::list<NukiLock::LogEntry>&
entry["action"] = "Unknown";
break;
}
memset(str, 0, sizeof(str));
NukiLock::completionStatusToString((NukiLock::CompletionStatus)log.data[2], str);
entry["completionStatus"] = str;
break;
}
@@ -622,6 +732,9 @@ void NetworkLock::publishConfig(const NukiLock::Config &config)
JsonDocument json;
memset(_nukiName, 0, sizeof(_nukiName));
memcpy(_nukiName, config.name, sizeof(config.name));
json["nukiID"] = uidString;
json["name"] = config.name;
//json["latitude"] = config.latitude;
@@ -742,7 +855,9 @@ void NetworkLock::publishBleAddress(const std::string &address)
void NetworkLock::publishKeypad(const std::list<NukiLock::KeypadEntry>& entries, uint maxKeypadCodeCount)
{
uint index = 0;
char uidString[20];
itoa(_preferences->getUInt(preference_nuki_id_lock, 0), uidString, 16);
String baseTopic = _preferences->getString(preference_mqtt_lock_path);
JsonDocument json;
for(const auto& entry : entries)
@@ -833,6 +948,51 @@ void NetworkLock::publishKeypad(const std::list<NukiLock::KeypadEntry>& entries,
sprintf(allowedUntilTimeT, "%02d:%02d", entry.allowedUntilTimeHour, entry.allowedUntilTimeMin);
jsonEntry["allowedUntilTime"] = allowedUntilTimeT;
if(_preferences->getBool(preference_keypad_topic_per_entry, false))
{
basePath = mqtt_topic_keypad;
basePath.concat("/codes/");
basePath.concat(std::to_string(index).c_str());
jsonEntry["name_ha"] = entry.name;
jsonEntry["index"] = index;
serializeJson(jsonEntry, _buffer, _bufferSize);
publishString(basePath.c_str(), _buffer);
String basePathPrefix = "~";
basePathPrefix.concat(basePath);
const char *basePathPrefixChr = basePathPrefix.c_str();
std::string baseCommand = std::string("{ \"action\": \"update\", \"codeId\": \"") + std::to_string(entry.codeId);
std::string enaCommand = baseCommand + (char*)"\", \"enabled\": \"1\" }";
std::string disCommand = baseCommand + (char*)"\", \"enabled\": \"0\" }";
std::string mqttDeviceName = std::string("keypad_") + std::to_string(index);
std::string uidStringPostfix = std::string("_") + mqttDeviceName;
char codeName[33];
memcpy(codeName, entry.name, sizeof(entry.name));
codeName[sizeof(entry.name)] = '\0';
std::string displayName = std::string("Keypad - ") + std::string((char*)codeName) + " - " + std::to_string(entry.codeId);
_network->publishHassTopic("switch",
mqttDeviceName.c_str(),
uidString,
uidStringPostfix.c_str(),
displayName.c_str(),
_nukiName,
baseTopic.c_str(),
String("~") + basePath.c_str(),
(char*)"SmartLock",
"",
"",
"diagnostic",
String("~") + mqtt_topic_keypad_json_action,
{ { (char*)"json_attr_t", (char*)basePathPrefixChr },
{ (char*)"pl_on", (char*)enaCommand.c_str() },
{ (char*)"pl_off", (char*)disCommand.c_str() },
{ (char*)"val_tpl", (char*)"{{value_json.enabled}}" },
{ (char*)"stat_on", (char*)"1" },
{ (char*)"stat_off", (char*)"0" }});
}
++index;
}
@@ -866,7 +1026,7 @@ void NetworkLock::publishKeypad(const std::list<NukiLock::KeypadEntry>& entries,
}
}
}
else if (maxKeypadCodeCount > 0)
else
{
for(int i=0; i<maxKeypadCodeCount; i++)
{
@@ -887,14 +1047,54 @@ void NetworkLock::publishKeypad(const std::list<NukiLock::KeypadEntry>& entries,
_network->removeTopic(codeTopic, "createdSec");
_network->removeTopic(codeTopic, "lockCount");
}
_preferences->putUInt(preference_lock_max_keypad_code_count, 0);
for(int j=entries.size(); j<maxKeypadCodeCount; j++)
{
String codesTopic = _mqttPath;
codesTopic.concat(mqtt_topic_keypad_codes);
codesTopic.concat("/");
String codeTopic = "code_";
codeTopic.concat(std::to_string(j).c_str());
_network->removeTopic(codesTopic, codeTopic);
std::string mqttDeviceName = std::string("keypad_") + std::to_string(j);
_network->removeHassTopic((char*)"switch", (char*)mqttDeviceName.c_str(), uidString);
}
}
}
void NetworkLock::publishTimeControl(const std::list<NukiLock::TimeControlEntry>& timeControlEntries)
void NetworkLock::publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry)
{
if(_preferences->getBool(preference_disable_non_json, false)) return;
char codeName[sizeof(entry.name) + 1];
memset(codeName, 0, sizeof(codeName));
memcpy(codeName, entry.name, sizeof(entry.name));
publishInt(concat(topic, "/id").c_str(), entry.codeId);
publishBool(concat(topic, "/enabled").c_str(), entry.enabled);
publishString(concat(topic, "/name").c_str(), codeName);
if(_preferences->getBool(preference_keypad_publish_code, false))
{
publishInt(concat(topic, "/code").c_str(), entry.code);
}
publishInt(concat(topic, "/createdYear").c_str(), entry.dateCreatedYear);
publishInt(concat(topic, "/createdMonth").c_str(), entry.dateCreatedMonth);
publishInt(concat(topic, "/createdDay").c_str(), entry.dateCreatedDay);
publishInt(concat(topic, "/createdHour").c_str(), entry.dateCreatedHour);
publishInt(concat(topic, "/createdMin").c_str(), entry.dateCreatedMin);
publishInt(concat(topic, "/createdSec").c_str(), entry.dateCreatedSec);
publishInt(concat(topic, "/lockCount").c_str(), entry.lockCount);
}
void NetworkLock::publishTimeControl(const std::list<NukiLock::TimeControlEntry>& timeControlEntries, uint maxTimeControlEntryCount)
{
uint index = 0;
char str[50];
char uidString[20];
itoa(_preferences->getUInt(preference_nuki_id_lock, 0), uidString, 16);
String baseTopic = _preferences->getString(preference_mqtt_lock_path);
JsonDocument json;
for(const auto& entry : timeControlEntries)
@@ -958,10 +1158,63 @@ void NetworkLock::publishTimeControl(const std::list<NukiLock::TimeControlEntry>
memset(str, 0, sizeof(str));
NukiLock::lockactionToString(entry.lockAction, str);
jsonEntry["lockAction"] = str;
if(_preferences->getBool(preference_timecontrol_topic_per_entry, false))
{
String basePath = mqtt_topic_timecontrol;
basePath.concat("/entries/");
basePath.concat(std::to_string(index).c_str());
jsonEntry["index"] = index;
serializeJson(jsonEntry, _buffer, _bufferSize);
publishString(basePath.c_str(), _buffer);
String basePathPrefix = "~";
basePathPrefix.concat(basePath);
const char *basePathPrefixChr = basePathPrefix.c_str();
std::string baseCommand = std::string("{ \"action\": \"update\", \"entryId\": \"") + std::to_string(entry.entryId);
std::string enaCommand = baseCommand + (char*)"\", \"enabled\": \"1\" }";
std::string disCommand = baseCommand + (char*)"\", \"enabled\": \"0\" }";
std::string mqttDeviceName = std::string("timecontrol_") + std::to_string(index);
std::string uidStringPostfix = std::string("_") + mqttDeviceName;
std::string displayName = std::string("Timecontrol - ") + std::to_string(entry.entryId);
_network->publishHassTopic("switch",
mqttDeviceName.c_str(),
uidString,
uidStringPostfix.c_str(),
displayName.c_str(),
_nukiName,
baseTopic.c_str(),
String("~") + basePath.c_str(),
(char*)"SmartLock",
"",
"",
"diagnostic",
String("~") + mqtt_topic_timecontrol_action,
{ { (char*)"json_attr_t", (char*)basePathPrefixChr },
{ (char*)"pl_on", (char*)enaCommand.c_str() },
{ (char*)"pl_off", (char*)disCommand.c_str() },
{ (char*)"val_tpl", (char*)"{{value_json.enabled}}" },
{ (char*)"stat_on", (char*)"1" },
{ (char*)"stat_off", (char*)"0" }});
}
++index;
}
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_timecontrol_json, _buffer);
for(int j=timeControlEntries.size(); j<maxTimeControlEntryCount; j++)
{
String entriesTopic = _mqttPath;
entriesTopic.concat(mqtt_topic_timecontrol_entries);
entriesTopic.concat("/");
_network->removeTopic(entriesTopic, (char*)std::to_string(j).c_str());
std::string mqttDeviceName = std::string("timecontrol_") + std::to_string(j);
_network->removeHassTopic((char*)"switch", (char*)mqttDeviceName.c_str(), uidString);
}
}
void NetworkLock::publishConfigCommandResult(const char* result)
@@ -995,6 +1248,11 @@ void NetworkLock::setLockActionReceivedCallback(LockActionResult (*lockActionRec
_lockActionReceivedCallback = lockActionReceivedCallback;
}
void NetworkLock::setOfficialUpdateReceivedCallback(void (*officialUpdateReceivedCallback)(const char *, const char *))
{
_officialUpdateReceivedCallback = officialUpdateReceivedCallback;
}
void NetworkLock::setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char *))
{
_configUpdateReceivedCallback = configUpdateReceivedCallback;
@@ -1016,10 +1274,15 @@ void NetworkLock::setTimeControlCommandReceivedCallback(void (*timeControlComman
_timeControlCommandReceivedReceivedCallback = timeControlCommandReceivedReceivedCallback;
}
void NetworkLock::buildMqttPath(const char* path, char* outPath)
void NetworkLock::buildMqttPath(const char* path, char* outPath, bool offPath)
{
int offset = 0;
for(const char& c : _mqttPath)
char inPath[181] = {0};
if(offPath) memcpy(inPath, _offMqttPath, sizeof(_offMqttPath));
else memcpy(inPath, _mqttPath, sizeof(_mqttPath));
for(const char& c : inPath)
{
if(c == 0x00)
{
@@ -1038,10 +1301,10 @@ void NetworkLock::buildMqttPath(const char* path, char* outPath)
outPath[i+1] = 0x00;
}
bool NetworkLock::comparePrefixedPath(const char *fullPath, const char *subPath)
bool NetworkLock::comparePrefixedPath(const char *fullPath, const char *subPath, bool offPath)
{
char prefixedPath[500];
buildMqttPath(subPath, prefixedPath);
buildMqttPath(subPath, prefixedPath, offPath);
return strcmp(fullPath, prefixedPath) == 0;
}
@@ -1087,76 +1350,55 @@ void NetworkLock::removeHASSConfig(char *uidString)
_network->removeHASSConfig(uidString);
}
void NetworkLock::publishFloat(const char *topic, const float value, const uint8_t precision)
void NetworkLock::publishOffAction(const int value)
{
_network->publishFloat(_mqttPath, topic, value, precision);
_network->publishInt(_offMqttPath, mqtt_topic_official_lock_action, value, false);
}
void NetworkLock::publishInt(const char *topic, const int value)
void NetworkLock::publishFloat(const char *topic, const float value, const uint8_t precision, bool retain)
{
_network->publishInt(_mqttPath, topic, value);
_network->publishFloat(_mqttPath, topic, value, precision, retain);
}
void NetworkLock::publishUInt(const char *topic, const unsigned int value)
void NetworkLock::publishInt(const char *topic, const int value, bool retain)
{
_network->publishUInt(_mqttPath, topic, value);
_network->publishInt(_mqttPath, topic, value, retain);
}
void NetworkLock::publishBool(const char *topic, const bool value)
void NetworkLock::publishUInt(const char *topic, const unsigned int value, bool retain)
{
_network->publishBool(_mqttPath, topic, value);
_network->publishUInt(_mqttPath, topic, value, retain);
}
bool NetworkLock::publishString(const char *topic, const String &value)
void NetworkLock::publishBool(const char *topic, const bool value, bool retain)
{
_network->publishBool(_mqttPath, topic, value, retain);
}
bool NetworkLock::publishString(const char *topic, const String &value, bool retain)
{
char str[value.length() + 1];
memset(str, 0, sizeof(str));
memcpy(str, value.begin(), value.length());
return publishString(topic, str);
return publishString(topic, str, retain);
}
bool NetworkLock::publishString(const char *topic, const std::string &value)
bool NetworkLock::publishString(const char *topic, const std::string &value, bool retain)
{
char str[value.size() + 1];
memset(str, 0, sizeof(str));
memcpy(str, value.data(), value.length());
return publishString(topic, str);
return publishString(topic, str, retain);
}
bool NetworkLock::publishString(const char *topic, const char *value)
bool NetworkLock::publishString(const char *topic, const char *value, bool retain)
{
return _network->publishString(_mqttPath, topic, value);
return _network->publishString(_mqttPath, topic, value, retain);
}
void NetworkLock::publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry)
void NetworkLock::publishULong(const char *topic, const unsigned long value, bool retain)
{
if(_preferences->getBool(preference_disable_non_json, false)) return;
char codeName[sizeof(entry.name) + 1];
memset(codeName, 0, sizeof(codeName));
memcpy(codeName, entry.name, sizeof(entry.name));
publishInt(concat(topic, "/id").c_str(), entry.codeId);
publishBool(concat(topic, "/enabled").c_str(), entry.enabled);
publishString(concat(topic, "/name").c_str(), codeName);
if(_preferences->getBool(preference_keypad_publish_code, false))
{
publishInt(concat(topic, "/code").c_str(), entry.code);
}
publishInt(concat(topic, "/createdYear").c_str(), entry.dateCreatedYear);
publishInt(concat(topic, "/createdMonth").c_str(), entry.dateCreatedMonth);
publishInt(concat(topic, "/createdDay").c_str(), entry.dateCreatedDay);
publishInt(concat(topic, "/createdHour").c_str(), entry.dateCreatedHour);
publishInt(concat(topic, "/createdMin").c_str(), entry.dateCreatedMin);
publishInt(concat(topic, "/createdSec").c_str(), entry.dateCreatedSec);
publishInt(concat(topic, "/lockCount").c_str(), entry.lockCount);
}
void NetworkLock::publishULong(const char *topic, const unsigned long value)
{
return _network->publishULong(_mqttPath, topic, value);
return _network->publishULong(_mqttPath, topic, value, retain);
}
String NetworkLock::concat(String a, String b)

View File

@@ -37,34 +37,59 @@ public:
void publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, const char *softwareVersion, const char *hardwareVersion, const bool& hasDoorSensor, const bool& hasKeypad, const bool& publishAuthData, char* lockAction, char* unlockAction, char* openAction);
void removeHASSConfig(char* uidString);
void publishKeypad(const std::list<NukiLock::KeypadEntry>& entries, uint maxKeypadCodeCount);
void publishTimeControl(const std::list<NukiLock::TimeControlEntry>& timeControlEntries);
void publishTimeControl(const std::list<NukiLock::TimeControlEntry>& timeControlEntries, uint maxTimeControlEntryCount);
void publishStatusUpdated(const bool statusUpdated);
void publishConfigCommandResult(const char* result);
void publishKeypadCommandResult(const char* result);
void publishKeypadJsonCommandResult(const char* result);
void publishTimeControlCommandResult(const char* result);
void publishOffAction(const int value);
void setLockActionReceivedCallback(LockActionResult (*lockActionReceivedCallback)(const char* value));
void setOfficialUpdateReceivedCallback(void (*officialUpdateReceivedCallback)(const char* path, const char* value));
void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* value));
void setKeypadCommandReceivedCallback(void (*keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled));
void setKeypadJsonCommandReceivedCallback(void (*keypadJsonCommandReceivedReceivedCallback)(const char* value));
void setTimeControlCommandReceivedCallback(void (*timeControlCommandReceivedReceivedCallback)(const char* value));
void onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) override;
void publishFloat(const char* topic, const float value, const uint8_t precision = 2, bool retain = false);
void publishInt(const char* topic, const int value, bool retain = false);
void publishUInt(const char* topic, const unsigned int value, bool retain = false);
void publishULong(const char* topic, const unsigned long value, bool retain = false);
void publishBool(const char* topic, const bool value, bool retain = false);
bool publishString(const char* topic, const String& value, bool retain = false);
bool publishString(const char* topic, const std::string& value, bool retain = false);
bool publishString(const char* topic, const char* value, bool retain = false);
bool reconnected();
uint8_t queryCommands();
//uint8_t _offMode = 0;
uint8_t _offState = 0;
bool _offCritical = false;
uint8_t _offChargeState = 100;
bool _offCharging = false;
bool _offKeypadCritical = false;
uint8_t _offDoorsensorState = 0;
bool _offDoorsensorCritical = false;
bool _offConnected = false;
uint8_t _offCommandResponse = 0;
char* _offLockActionEvent;
uint8_t _offLockAction = 0;
uint8_t _offTrigger = 0;
uint32_t _offAuthId = 0;
uint32_t _offCodeId = 0;
uint8_t _offContext = 0;
uint32_t _authId = 0;
unsigned long _offCommandExecutedTs = 0;
NukiLock::LockAction _offCommand = (NukiLock::LockAction)0xff;
char _nukiName[33];
char _authName[33];
bool _authFound = false;
private:
bool comparePrefixedPath(const char* fullPath, const char* subPath);
bool comparePrefixedPath(const char* fullPath, const char* subPath, bool offPath = false);
void publishFloat(const char* topic, const float value, const uint8_t precision = 2);
void publishInt(const char* topic, const int value);
void publishUInt(const char* topic, const unsigned int value);
void publishULong(const char* topic, const unsigned long value);
void publishBool(const char* topic, const bool value);
bool publishString(const char* topic, const String& value);
bool publishString(const char* topic, const std::string& value);
bool publishString(const char* topic, const char* value);
void publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry);
void buttonPressActionToString(const NukiLock::ButtonPressAction btnPressAction, char* str);
void homeKitStatusToString(const int hkstatus, char* str);
@@ -72,12 +97,14 @@ private:
String concat(String a, String b);
void buildMqttPath(const char* path, char* outPath);
void buildMqttPath(const char* path, char* outPath, bool offPath = false);
Network* _network;
Preferences* _preferences;
std::vector<char*> _offTopics;
char _mqttPath[181] = {0};
char _offMqttPath[181] = {0};
bool _firstTunerStatePublish = true;
unsigned long _lastMaintenanceTs = 0;
@@ -89,15 +116,13 @@ private:
uint _keypadCommandId = 0;
int _keypadCommandEnabled = 1;
uint8_t _queryCommands = 0;
uint32_t _authId = 0;
char _authName[33];
bool _authFound = false;
uint32_t _lastRollingLog = 0;
char* _buffer;
size_t _bufferSize;
LockActionResult (*_lockActionReceivedCallback)(const char* value) = nullptr;
void (*_officialUpdateReceivedCallback)(const char* path, const char* value) = nullptr;
void (*_configUpdateReceivedCallback)(const char* value) = nullptr;
void (*_keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled) = nullptr;
void (*_keypadJsonCommandReceivedReceivedCallback)(const char* value) = nullptr;

View File

@@ -68,6 +68,18 @@ void NetworkOpener::initialize()
_network->removeTopic(_mqttPath, mqtt_topic_battery_keypad_critical);
//_network->removeTopic(_mqttPath, mqtt_topic_presence);
}
if(!_preferences->getBool(preference_conf_info_enabled, false))
{
_network->removeTopic(_mqttPath, mqtt_topic_config_basic_json);
_network->removeTopic(_mqttPath, mqtt_topic_config_advanced_json);
_network->removeTopic(_mqttPath, mqtt_topic_config_button_enabled);
_network->removeTopic(_mqttPath, mqtt_topic_config_led_enabled);
_network->removeTopic(_mqttPath, mqtt_topic_config_led_brightness);
_network->removeTopic(_mqttPath, mqtt_topic_config_auto_unlock);
_network->removeTopic(_mqttPath, mqtt_topic_config_auto_lock);
_network->removeTopic(_mqttPath, mqtt_topic_config_single_lock);
}
if(_preferences->getBool(preference_keypad_control_enabled))
{
@@ -397,12 +409,15 @@ void NetworkOpener::publishState(NukiOpener::OpenerState lockState)
publishString(mqtt_topic_lock_binary_state, "locked");
break;
case NukiOpener::LockState::RTOactive:
case NukiOpener::LockState::Open:
publishString(mqtt_topic_lock_ha_state, "unlocked");
publishString(mqtt_topic_lock_binary_state, "unlocked");
break;
case NukiOpener::LockState::Open:
publishString(mqtt_topic_lock_ha_state, "open");
publishString(mqtt_topic_lock_binary_state, "unlocked");
break;
case NukiOpener::LockState::Opening:
publishString(mqtt_topic_lock_ha_state, "unlocking");
publishString(mqtt_topic_lock_ha_state, "opening");
publishString(mqtt_topic_lock_binary_state, "unlocked");
break;
case NukiOpener::LockState::Undefined:
@@ -479,10 +494,34 @@ void NetworkOpener::publishAuthorizationInfo(const std::list<NukiOpener::LogEntr
memset(str, 0, sizeof(str));
NukiOpener::lockactionToString((NukiOpener::LockAction)log.data[0], str);
entry["action"] = str;
switch(log.data[1])
{
case 0:
entry["trigger"] = "arrowkey";
break;
case 1:
entry["trigger"] = "code";
break;
case 2:
entry["trigger"] = "fingerprint";
break;
default:
entry["trigger"] = "Unknown";
break;
}
memset(str, 0, sizeof(str));
NukiOpener::completionStatusToString((NukiOpener::CompletionStatus)log.data[2], str);
entry["completionStatus"] = str;
if(log.data[2] == 9) entry["completionStatus"] = "notAuthorized";
else if (log.data[2] == 224) entry["completionStatus"] = "invalidCode";
else
{
NukiOpener::completionStatusToString((NukiOpener::CompletionStatus)log.data[2], str);
entry["completionStatus"] = str;
}
entry["codeId"] = 256U*log.data[4]+log.data[3];
break;
case NukiOpener::LoggingType::DoorbellRecognition:
switch(log.data[0] & 3)
@@ -530,8 +569,11 @@ void NetworkOpener::publishAuthorizationInfo(const std::list<NukiOpener::LogEntr
entry["geofence"] = log.data[2] == 1 ? "active" : "inactive";
entry["doorbellSuppression"] = log.data[3] == 1 ? "active" : "inactive";
entry["soundId"] = log.data[4];
memset(str, 0, sizeof(str));
NukiOpener::completionStatusToString((NukiOpener::CompletionStatus)log.data[5], str);
entry["completionStatus"] = str;
entry["codeId"] = 256U*log.data[7]+log.data[6];
break;
}
@@ -605,6 +647,9 @@ void NetworkOpener::publishConfig(const NukiOpener::Config &config)
itoa(config.nukiId, uidString, 16);
JsonDocument json;
memset(_nukiName, 0, sizeof(_nukiName));
memcpy(_nukiName, config.name, sizeof(config.name));
json["nukiID"] = uidString;
json["name"] = config.name;
@@ -758,7 +803,9 @@ void NetworkOpener::removeHASSConfig(char* uidString)
void NetworkOpener::publishKeypad(const std::list<NukiLock::KeypadEntry>& entries, uint maxKeypadCodeCount)
{
uint index = 0;
char uidString[20];
itoa(_preferences->getUInt(preference_nuki_id_opener, 0), uidString, 16);
String baseTopic = _preferences->getString(preference_mqtt_opener_path);
JsonDocument json;
for(const auto& entry : entries)
@@ -771,14 +818,14 @@ void NetworkOpener::publishKeypad(const std::list<NukiLock::KeypadEntry>& entrie
auto jsonEntry = json.add<JsonVariant>();
jsonEntry["codeId"] = entry.codeId;
jsonEntry["enabled"] = entry.enabled;
jsonEntry["name"] = entry.name;
if(_preferences->getBool(preference_keypad_publish_code, false))
{
jsonEntry["code"] = entry.code;
}
jsonEntry["enabled"] = entry.enabled;
jsonEntry["name"] = entry.name;
char createdDT[20];
sprintf(createdDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.dateCreatedYear, entry.dateCreatedMonth, entry.dateCreatedDay, entry.dateCreatedHour, entry.dateCreatedMin, entry.dateCreatedSec);
jsonEntry["dateCreated"] = createdDT;
@@ -849,6 +896,51 @@ void NetworkOpener::publishKeypad(const std::list<NukiLock::KeypadEntry>& entrie
sprintf(allowedUntilTimeT, "%02d:%02d", entry.allowedUntilTimeHour, entry.allowedUntilTimeMin);
jsonEntry["allowedUntilTime"] = allowedUntilTimeT;
if(_preferences->getBool(preference_keypad_topic_per_entry, false))
{
basePath = mqtt_topic_keypad;
basePath.concat("/codes/");
basePath.concat(std::to_string(index).c_str());
jsonEntry["name_ha"] = entry.name;
jsonEntry["index"] = index;
serializeJson(jsonEntry, _buffer, _bufferSize);
publishString(basePath.c_str(), _buffer);
String basePathPrefix = "~";
basePathPrefix.concat(basePath);
const char *basePathPrefixChr = basePathPrefix.c_str();
std::string baseCommand = std::string("{ \"action\": \"update\", \"codeId\": \"") + std::to_string(entry.codeId);
std::string enaCommand = baseCommand + (char*)"\", \"enabled\": \"1\" }";
std::string disCommand = baseCommand + (char*)"\", \"enabled\": \"0\" }";
std::string mqttDeviceName = std::string("keypad_") + std::to_string(index);
std::string uidStringPostfix = std::string("_") + mqttDeviceName;
char codeName[33];
memcpy(codeName, entry.name, sizeof(entry.name));
codeName[sizeof(entry.name)] = '\0';
std::string displayName = std::string("Keypad - ") + std::string((char*)codeName) + " - " + std::to_string(entry.codeId);
_network->publishHassTopic("switch",
mqttDeviceName.c_str(),
uidString,
uidStringPostfix.c_str(),
displayName.c_str(),
_nukiName,
baseTopic.c_str(),
String("~") + basePath.c_str(),
(char*)"SmartLock",
"",
"",
"diagnostic",
String("~") + mqtt_topic_keypad_json_action,
{ { (char*)"json_attr_t", (char*)basePathPrefixChr },
{ (char*)"pl_on", (char*)enaCommand.c_str() },
{ (char*)"pl_off", (char*)disCommand.c_str() },
{ (char*)"val_tpl", (char*)"{{value_json.enabled}}" },
{ (char*)"stat_on", (char*)"1" },
{ (char*)"stat_off", (char*)"0" }});
}
++index;
}
@@ -868,7 +960,7 @@ void NetworkOpener::publishKeypad(const std::list<NukiLock::KeypadEntry>& entrie
++index;
}
if(!_preferences->getBool(preference_keypad_publish_code, false))
{
for(int i=0; i<maxKeypadCodeCount; i++)
@@ -882,7 +974,7 @@ void NetworkOpener::publishKeypad(const std::list<NukiLock::KeypadEntry>& entrie
}
}
}
else if (maxKeypadCodeCount > 0)
else
{
for(int i=0; i<maxKeypadCodeCount; i++)
{
@@ -903,14 +995,26 @@ void NetworkOpener::publishKeypad(const std::list<NukiLock::KeypadEntry>& entrie
_network->removeTopic(codeTopic, "createdSec");
_network->removeTopic(codeTopic, "lockCount");
}
_preferences->putUInt(preference_lock_max_keypad_code_count, 0);
for(int j=entries.size(); j<maxKeypadCodeCount; j++)
{
String codesTopic = _mqttPath;
codesTopic.concat(mqtt_topic_keypad_codes);
codesTopic.concat("/");
_network->removeTopic(codesTopic, (char*)std::to_string(j).c_str());
std::string mqttDeviceName = std::string("keypad_") + std::to_string(j);
_network->removeHassTopic((char*)"switch", (char*)mqttDeviceName.c_str(), uidString);
}
}
}
void NetworkOpener::publishTimeControl(const std::list<NukiOpener::TimeControlEntry>& timeControlEntries)
void NetworkOpener::publishTimeControl(const std::list<NukiOpener::TimeControlEntry>& timeControlEntries, uint maxTimeControlEntryCount)
{
uint index = 0;
char str[50];
char uidString[20];
itoa(_preferences->getUInt(preference_nuki_id_opener, 0), uidString, 16);
String baseTopic = _preferences->getString(preference_mqtt_opener_path);
JsonDocument json;
for(const auto& entry : timeControlEntries)
@@ -974,10 +1078,62 @@ void NetworkOpener::publishTimeControl(const std::list<NukiOpener::TimeControlEn
memset(str, 0, sizeof(str));
NukiOpener::lockactionToString(entry.lockAction, str);
jsonEntry["lockAction"] = str;
if(_preferences->getBool(preference_timecontrol_topic_per_entry, false))
{
String basePath = mqtt_topic_timecontrol;
basePath.concat("/entries/");
basePath.concat(std::to_string(index).c_str());
jsonEntry["index"] = index;
serializeJson(jsonEntry, _buffer, _bufferSize);
publishString(basePath.c_str(), _buffer);
String basePathPrefix = "~";
basePathPrefix.concat(basePath);
const char *basePathPrefixChr = basePathPrefix.c_str();
std::string baseCommand = std::string("{ \"action\": \"update\", \"entryId\": \"") + std::to_string(entry.entryId);
std::string enaCommand = baseCommand + (char*)"\", \"enabled\": \"1\" }";
std::string disCommand = baseCommand + (char*)"\", \"enabled\": \"0\" }";
std::string mqttDeviceName = std::string("timecontrol_") + std::to_string(index);
std::string uidStringPostfix = std::string("_") + mqttDeviceName;
std::string displayName = std::string("Timecontrol - ") + std::to_string(entry.entryId);
_network->publishHassTopic("switch",
mqttDeviceName.c_str(),
uidString,
uidStringPostfix.c_str(),
displayName.c_str(),
_nukiName,
baseTopic.c_str(),
String("~") + basePath.c_str(),
(char*)"Opener",
"",
"",
"diagnostic",
String("~") + mqtt_topic_timecontrol_action,
{ { (char*)"json_attr_t", (char*)basePathPrefixChr },
{ (char*)"pl_on", (char*)enaCommand.c_str() },
{ (char*)"pl_off", (char*)disCommand.c_str() },
{ (char*)"val_tpl", (char*)"{{value_json.enabled}}" },
{ (char*)"stat_on", (char*)"1" },
{ (char*)"stat_off", (char*)"0" }});
}
++index;
}
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_timecontrol_json, _buffer);
for(int j=timeControlEntries.size(); j<maxTimeControlEntryCount; j++)
{
String entriesTopic = _mqttPath;
entriesTopic.concat(mqtt_topic_timecontrol_entries);
entriesTopic.concat("/");
_network->removeTopic(entriesTopic, (char*)std::to_string(j).c_str());
std::string mqttDeviceName = std::string("timecontrol_") + std::to_string(j);
_network->removeHassTopic((char*)"switch", (char*)mqttDeviceName.c_str(), uidString);
}
}
void NetworkOpener::publishConfigCommandResult(const char* result)
@@ -1032,45 +1188,45 @@ void NetworkOpener::setTimeControlCommandReceivedCallback(void (*timeControlComm
_timeControlCommandReceivedReceivedCallback = timeControlCommandReceivedReceivedCallback;
}
void NetworkOpener::publishFloat(const char *topic, const float value, const uint8_t precision)
void NetworkOpener::publishFloat(const char *topic, const float value, const uint8_t precision, bool retain)
{
_network->publishFloat(_mqttPath, topic, value, precision);
_network->publishFloat(_mqttPath, topic, value, precision, retain);
}
void NetworkOpener::publishInt(const char *topic, const int value)
void NetworkOpener::publishInt(const char *topic, const int value, bool retain)
{
_network->publishInt(_mqttPath, topic, value);
_network->publishInt(_mqttPath, topic, value, retain);
}
void NetworkOpener::publishUInt(const char *topic, const unsigned int value)
void NetworkOpener::publishUInt(const char *topic, const unsigned int value, bool retain)
{
_network->publishUInt(_mqttPath, topic, value);
_network->publishUInt(_mqttPath, topic, value, retain);
}
void NetworkOpener::publishBool(const char *topic, const bool value)
void NetworkOpener::publishBool(const char *topic, const bool value, bool retain)
{
_network->publishBool(_mqttPath, topic, value);
_network->publishBool(_mqttPath, topic, value, retain);
}
void NetworkOpener::publishString(const char *topic, const String &value)
void NetworkOpener::publishString(const char *topic, const String &value, bool retain)
{
char str[value.length() + 1];
memset(str, 0, sizeof(str));
memcpy(str, value.begin(), value.length());
publishString(topic, str);
publishString(topic, str, retain);
}
void NetworkOpener::publishString(const char *topic, const std::string &value)
void NetworkOpener::publishString(const char *topic, const std::string &value, bool retain)
{
char str[value.size() + 1];
memset(str, 0, sizeof(str));
memcpy(str, value.data(), value.length());
publishString(topic, str);
publishString(topic, str, retain);
}
void NetworkOpener::publishString(const char* topic, const char* value)
void NetworkOpener::publishString(const char* topic, const char* value, bool retain)
{
_network->publishString(_mqttPath, topic, value);
_network->publishString(_mqttPath, topic, value, retain);
}
void NetworkOpener::publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry)

View File

@@ -34,7 +34,7 @@ public:
void publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, const char *softwareVersion, const char *hardwareVersion, const bool& publishAuthData, const bool& hasKeypad, char* lockAction, char* unlockAction, char* openAction);
void removeHASSConfig(char* uidString);
void publishKeypad(const std::list<NukiLock::KeypadEntry>& entries, uint maxKeypadCodeCount);
void publishTimeControl(const std::list<NukiOpener::TimeControlEntry>& timeControlEntries);
void publishTimeControl(const std::list<NukiOpener::TimeControlEntry>& timeControlEntries, uint maxTimeControlEntryCount);
void publishStatusUpdated(const bool statusUpdated);
void publishConfigCommandResult(const char* result);
void publishKeypadCommandResult(const char* result);
@@ -50,17 +50,18 @@ public:
bool reconnected();
uint8_t queryCommands();
char _nukiName[33];
private:
bool comparePrefixedPath(const char* fullPath, const char* subPath);
void publishFloat(const char* topic, const float value, const uint8_t precision = 2);
void publishInt(const char* topic, const int value);
void publishUInt(const char* topic, const unsigned int value);
void publishBool(const char* topic, const bool value);
void publishString(const char* topic, const String& value);
void publishString(const char* topic, const std::string& value);
void publishString(const char* topic, const char* value);
void publishFloat(const char* topic, const float value, const uint8_t precision = 2, bool retain = false);
void publishInt(const char* topic, const int value, bool retain = false);
void publishUInt(const char* topic, const unsigned int value, bool retain = false);
void publishBool(const char* topic, const bool value, bool retain = false);
void publishString(const char* topic, const String& value, bool retain = false);
void publishString(const char* topic, const std::string& value, bool retain = false);
void publishString(const char* topic, const char* value, bool retain = false);
void publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry);
void buildMqttPath(const char* path, char* outPath);

File diff suppressed because it is too large Load Diff

View File

@@ -100,6 +100,7 @@ private:
int _restartBeaconTimeout = 0; // seconds
bool _publishAuthData = false;
bool _clearAuthData = false;
bool _taskRunning = false;
int _nrOfRetries = 0;
int _retryDelay = 0;
int _retryCount = 0;
@@ -107,7 +108,6 @@ private:
int _retryLockstateCount = 0;
unsigned long _nextRetryTs = 0;
std::vector<uint16_t> _keypadCodeIds;
std::vector<uint32_t> _keypadCodes;
std::vector<uint8_t> _timeControlIds;
NukiOpener::OpenerState _lastKeyTurnerState;
@@ -128,6 +128,7 @@ private:
bool _hasKeypad = false;
bool _keypadEnabled = false;
uint _maxKeypadCodeCount = 0;
uint _maxTimeControlEntryCount = 0;
bool _configRead = false;
long _rssiPublishInterval = 0;
unsigned long _nextLockStateUpdateTs = 0;

File diff suppressed because it is too large Load Diff

View File

@@ -46,12 +46,14 @@ public:
private:
static LockActionResult onLockActionReceivedCallback(const char* value);
static void onOfficialUpdateReceivedCallback(const char* topic, const char* value);
static void onConfigUpdateReceivedCallback(const char* value);
static void onKeypadCommandReceivedCallback(const char* command, const uint& id, const String& name, const String& code, const int& enabled);
static void onKeypadJsonCommandReceivedCallback(const char* value);
static void onTimeControlCommandReceivedCallback(const char* value);
static void gpioActionCallback(const GpioAction& action, const int& pin);
void onKeypadCommandReceived(const char* command, const uint& id, const String& name, const String& code, const int& enabled);
void onOfficialUpdateReceived(const char* topic, const char* value);
void onConfigUpdateReceived(const char* value);
void onKeypadJsonCommandReceived(const char* value);
void onTimeControlCommandReceived(const char* value);
@@ -88,14 +90,15 @@ private:
Gpio* _gpio = nullptr;
Preferences* _preferences;
int _intervalLockstate = 0; // seconds
int _intervalHybridLockstate = 0; // seconds
int _intervalBattery = 0; // seconds
int _intervalConfig = 60 * 60; // seconds
int _intervalKeypad = 0; // seconds
int _restartBeaconTimeout = 0; // seconds
bool _publishAuthData = false;
bool _clearAuthData = false;
bool _taskRunning = false;
std::vector<uint16_t> _keypadCodeIds;
std::vector<uint32_t> _keypadCodes;
std::vector<uint8_t> _timeControlIds;
NukiLock::KeyTurnerState _lastKeyTurnerState;
@@ -116,6 +119,7 @@ private:
bool _hasKeypad = false;
bool _keypadEnabled = false;
uint _maxKeypadCodeCount = 0;
uint _maxTimeControlEntryCount = 0;
bool _configRead = false;
int _nrOfRetries = 0;
int _retryDelay = 0;
@@ -123,8 +127,9 @@ private:
int _retryConfigCount = 0;
int _retryLockstateCount = 0;
long _rssiPublishInterval = 0;
unsigned long _nextRetryTs = 0;
unsigned long _nextRetryTs = 0;
unsigned long _nextLockStateUpdateTs = 0;
unsigned long _nextHybridLockStateUpdateTs = 0;
unsigned long _nextBatteryReportTs = 0;
unsigned long _nextConfigUpdateTs = 0;
unsigned long _waitAuthLogUpdateTs = 0;

View File

@@ -24,6 +24,8 @@
#define preference_check_updates (char*)"checkupdates"
#define preference_lock_max_keypad_code_count (char*)"maxkpad"
#define preference_opener_max_keypad_code_count (char*)"opmaxkpad"
#define preference_lock_max_timecontrol_entry_count (char*)"maxtc"
#define preference_opener_max_timecontrol_entry_count (char*)"opmaxtc"
#define preference_mqtt_ca (char*)"mqttca"
#define preference_mqtt_crt (char*)"mqttcrt"
#define preference_mqtt_key (char*)"mqttkey"
@@ -50,17 +52,21 @@
#define preference_query_interval_keypad (char*)"kpInterval"
#define preference_access_level (char*)"accLvl"
#define preference_keypad_info_enabled (char*)"kpInfoEnabled"
#define preference_keypad_topic_per_entry (char*)"kpPerEntry"
#define preference_keypad_control_enabled (char*)"kpCntrlEnabled"
#define preference_keypad_publish_code (char*)"kpPubCode"
#define preference_timecontrol_control_enabled (char*)"tcCntrlEnabled"
#define preference_timecontrol_topic_per_entry (char*)"tcPerEntry"
#define preference_timecontrol_info_enabled (char*)"tcInfoEnabled"
#define preference_publish_authdata (char*)"pubAuth"
#define preference_acl (char*)"aclLckOpn"
#define preference_conf_info_enabled (char*)"cnfInfoEnabled"
#define preference_conf_lock_basic_acl (char*)"confLckBasAcl"
#define preference_conf_lock_advanced_acl (char*)"confLckAdvAcl"
#define preference_conf_opener_basic_acl (char*)"confOpnBasAcl"
#define preference_conf_opener_advanced_acl (char*)"confOpnAdvAcl"
#define preference_register_as_app (char*)"regAsApp" // true = register as hub; false = register as app
#define preference_register_opener_as_app (char*)"regOpnAsApp"
#define preference_command_nr_of_retries (char*)"nrRetry"
#define preference_command_retry_delay (char*)"rtryDelay"
#define preference_cred_user (char*)"crdusr"
@@ -84,6 +90,10 @@
#define preference_enable_bootloop_reset (char*)"enabtlprst"
#define preference_buffer_size (char*)"buffsize"
#define preference_disable_non_json (char*)"disnonjson"
#define preference_official_hybrid (char*)"offHybrid"
#define preference_official_hybrid_actions (char*)"hybridAct"
#define preference_official_hybrid_retry (char*)"hybridRtry"
#define preference_query_interval_hybrid_lockstate (char*)"hybridTimer"
class DebugPreferences
{
@@ -92,18 +102,18 @@ private:
{
preference_started_before, preference_config_version, preference_device_id_lock, preference_device_id_opener, preference_nuki_id_lock, preference_nuki_id_opener, preference_mqtt_broker, preference_mqtt_broker_port, preference_mqtt_user, preference_mqtt_password, preference_mqtt_log_enabled, preference_check_updates, preference_webserver_enabled,
preference_lock_enabled, preference_lock_pin_status, preference_mqtt_lock_path, preference_opener_enabled, preference_opener_pin_status,
preference_opener_continuous_mode, preference_mqtt_opener_path, preference_lock_max_keypad_code_count, preference_opener_max_keypad_code_count,
preference_enable_bootloop_reset, preference_mqtt_ca, preference_mqtt_crt, preference_mqtt_key, preference_mqtt_hass_discovery, preference_mqtt_hass_cu_url,
preference_buffer_size, preference_ip_dhcp_enabled, preference_ip_address, preference_ip_subnet, preference_ip_gateway, preference_ip_dns_server,
preference_network_hardware, preference_network_wifi_fallback_disabled, preference_rssi_publish_interval, preference_hostname, preference_find_best_rssi,
preference_network_timeout, preference_restart_on_disconnect, preference_restart_ble_beacon_lost, preference_query_interval_lockstate,
preference_query_interval_configuration, preference_query_interval_battery, preference_query_interval_keypad, preference_keypad_control_enabled,
preference_keypad_info_enabled, preference_keypad_publish_code, preference_acl, preference_timecontrol_control_enabled, preference_timecontrol_info_enabled,
preference_conf_lock_basic_acl, preference_conf_lock_advanced_acl, preference_conf_opener_basic_acl, preference_conf_opener_advanced_acl,
preference_access_level, preference_register_as_app, preference_command_nr_of_retries, preference_command_retry_delay, preference_cred_user,
preference_cred_password, preference_publish_authdata, preference_publish_debug_info, preference_presence_detection_timeout, preference_disable_non_json,
preference_has_mac_saved, preference_has_mac_byte_0, preference_has_mac_byte_1, preference_has_mac_byte_2, preference_latest_version,
preference_task_size_network, preference_task_size_nuki, preference_task_size_pd, preference_authlog_max_entries, preference_keypad_max_entries, preference_timecontrol_max_entries
preference_opener_continuous_mode, preference_mqtt_opener_path, preference_lock_max_keypad_code_count, preference_opener_max_keypad_code_count, preference_lock_max_timecontrol_entry_count,
preference_opener_max_timecontrol_entry_count, preference_enable_bootloop_reset, preference_mqtt_ca, preference_mqtt_crt, preference_mqtt_key, preference_mqtt_hass_discovery,
preference_mqtt_hass_cu_url, preference_buffer_size, preference_ip_dhcp_enabled, preference_ip_address, preference_ip_subnet, preference_ip_gateway, preference_ip_dns_server,
preference_network_hardware, preference_network_wifi_fallback_disabled, preference_rssi_publish_interval, preference_hostname, preference_find_best_rssi,
preference_network_timeout, preference_restart_on_disconnect, preference_restart_ble_beacon_lost, preference_query_interval_lockstate, preference_timecontrol_topic_per_entry,
preference_keypad_topic_per_entry, preference_query_interval_configuration, preference_query_interval_battery, preference_query_interval_keypad, preference_keypad_control_enabled,
preference_keypad_info_enabled, preference_keypad_publish_code, preference_timecontrol_control_enabled, preference_timecontrol_info_enabled, preference_conf_info_enabled,
preference_register_as_app, preference_register_opener_as_app, preference_command_nr_of_retries, preference_command_retry_delay, preference_cred_user, preference_cred_password,
preference_disable_non_json, preference_publish_authdata, preference_publish_debug_info, preference_presence_detection_timeout, preference_official_hybrid, preference_query_interval_hybrid_lockstate,
preference_official_hybrid_actions, preference_official_hybrid_retry, preference_has_mac_saved, preference_has_mac_byte_0, preference_has_mac_byte_1, preference_has_mac_byte_2,
preference_latest_version, preference_task_size_network, preference_task_size_nuki, preference_task_size_pd, preference_authlog_max_entries, preference_keypad_max_entries,
preference_timecontrol_max_entries
};
std::vector<char*> _redact =
{
@@ -115,9 +125,10 @@ private:
std::vector<char*> _boolPrefs =
{
preference_started_before, preference_mqtt_log_enabled, preference_check_updates, preference_lock_enabled, preference_opener_enabled, preference_opener_continuous_mode,
preference_enable_bootloop_reset, preference_webserver_enabled, preference_find_best_rssi, preference_restart_on_disconnect, preference_keypad_control_enabled,
preference_keypad_info_enabled, preference_keypad_publish_code, preference_timecontrol_control_enabled, preference_timecontrol_info_enabled, preference_register_as_app,
preference_ip_dhcp_enabled, preference_publish_authdata, preference_has_mac_saved, preference_publish_debug_info, preference_network_wifi_fallback_disabled, preference_disable_non_json
preference_timecontrol_topic_per_entry, preference_keypad_topic_per_entry, preference_enable_bootloop_reset, preference_webserver_enabled, preference_find_best_rssi, preference_restart_on_disconnect,
preference_keypad_control_enabled, preference_keypad_info_enabled, preference_keypad_publish_code, preference_timecontrol_control_enabled, preference_timecontrol_info_enabled,
preference_register_as_app, preference_register_opener_as_app, preference_ip_dhcp_enabled, preference_publish_authdata, preference_has_mac_saved, preference_publish_debug_info,
preference_network_wifi_fallback_disabled, preference_official_hybrid, preference_official_hybrid_actions, preference_official_hybrid_retry, preference_conf_info_enabled, preference_disable_non_json
};
const bool isRedacted(const char* key) const

View File

@@ -430,6 +430,28 @@ bool WebCfgServer::processArgs(String& message)
_preferences->putBool(preference_check_updates, (value == "1"));
configChanged = true;
}
else if(key == "OFFHYBRID")
{
_preferences->putBool(preference_official_hybrid, (value == "1"));
if((value == "1")) _preferences->putBool(preference_register_as_app, true);
configChanged = true;
}
else if(key == "HYBRIDACT")
{
_preferences->putBool(preference_official_hybrid_actions, (value == "1"));
if(value == "1") _preferences->putBool(preference_register_as_app, true);
configChanged = true;
}
else if(key == "HYBRIDTIMER")
{
_preferences->putInt(preference_query_interval_hybrid_lockstate, value.toInt());
configChanged = true;
}
else if(key == "HYBRIDRETRY")
{
_preferences->putBool(preference_official_hybrid_retry, (value == "1"));
configChanged = true;
}
else if(key == "DISNONJSON")
{
_preferences->putBool(preference_disable_non_json, (value == "1"));
@@ -565,6 +587,11 @@ bool WebCfgServer::processArgs(String& message)
{
aclLvlChanged = true;
}
else if(key == "CONFPUB")
{
_preferences->putBool(preference_conf_info_enabled, (value == "1"));
configChanged = true;
}
else if(key == "KPPUB")
{
_preferences->putBool(preference_keypad_info_enabled, (value == "1"));
@@ -585,6 +612,16 @@ bool WebCfgServer::processArgs(String& message)
_preferences->putBool(preference_timecontrol_info_enabled, (value == "1"));
configChanged = true;
}
else if(key == "KPPER")
{
_preferences->putBool(preference_keypad_topic_per_entry, (value == "1"));
configChanged = true;
}
else if(key == "TCPER")
{
_preferences->putBool(preference_timecontrol_topic_per_entry, (value == "1"));
configChanged = true;
}
else if(key == "TCENA")
{
_preferences->putBool(preference_timecontrol_control_enabled, (value == "1"));
@@ -956,6 +993,11 @@ bool WebCfgServer::processArgs(String& message)
_preferences->putBool(preference_register_as_app, (value == "1"));
configChanged = true;
}
else if(key == "REGAPPOPN")
{
_preferences->putBool(preference_register_opener_as_app, (value == "1"));
configChanged = true;
}
else if(key == "LOCKENA")
{
_preferences->putBool(preference_lock_enabled, (value == "1"));
@@ -1177,9 +1219,9 @@ void WebCfgServer::buildCredHtml(String &response)
response.concat("<form class=\"adapt\" method=\"post\" action=\"savecfg\">");
response.concat("<h3>Credentials</h3>");
response.concat("<table>");
printInputField(response, "CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30, false, true);
printInputField(response, "CREDPASS", "Password", "*", 30, true, true);
printInputField(response, "CREDPASSRE", "Retype password", "*", 30, true);
printInputField(response, "CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30, "", false, true);
printInputField(response, "CREDPASS", "Password", "*", 30, "", true, true);
printInputField(response, "CREDPASSRE", "Retype password", "*", 30, "", true);
response.concat("</table>");
response.concat("<br><input type=\"submit\" name=\"submit\" value=\"Save\">");
response.concat("</form>");
@@ -1189,7 +1231,7 @@ void WebCfgServer::buildCredHtml(String &response)
response.concat("<br><br><form class=\"adapt\" method=\"post\" action=\"savecfg\">");
response.concat("<h3>Nuki Lock PIN</h3>");
response.concat("<table>");
printInputField(response, "NUKIPIN", "PIN Code (# to clear)", "*", 20, true);
printInputField(response, "NUKIPIN", "PIN Code (# to clear)", "*", 20, "", true);
response.concat("</table>");
response.concat("<br><input type=\"submit\" name=\"submit\" value=\"Save\">");
response.concat("</form>");
@@ -1200,7 +1242,7 @@ void WebCfgServer::buildCredHtml(String &response)
response.concat("<br><br><form class=\"adapt\" method=\"post\" action=\"savecfg\">");
response.concat("<h3>Nuki Opener PIN</h3>");
response.concat("<table>");
printInputField(response, "NUKIOPPIN", "PIN Code (# to clear)", "*", 20, true);
printInputField(response, "NUKIOPPIN", "PIN Code (# to clear)", "*", 20, "", true);
response.concat("</table>");
response.concat("<br><input type=\"submit\" name=\"submit\" value=\"Save\">");
response.concat("</form>");
@@ -1215,7 +1257,7 @@ void WebCfgServer::buildCredHtml(String &response)
String message = "Type ";
message.concat(_confirmCode);
message.concat(" to confirm unpair");
printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10);
printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10, "");
response.concat("</table>");
response.concat("<br><button type=\"submit\">OK</button></form>");
}
@@ -1228,7 +1270,7 @@ void WebCfgServer::buildCredHtml(String &response)
String message = "Type ";
message.concat(_confirmCode);
message.concat(" to confirm unpair");
printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10);
printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10, "");
response.concat("</table>");
response.concat("<br><button type=\"submit\">OK</button></form>");
}
@@ -1240,7 +1282,7 @@ void WebCfgServer::buildCredHtml(String &response)
String message = "Type ";
message.concat(_confirmCode);
message.concat(" to confirm factory reset");
printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10);
printInputField(response, "CONFIRMTOKEN", message.c_str(), "", 10, "");
printCheckBox(response, "WIFI", "Also reset WiFi settings", false, "");
response.concat("</table>");
response.concat("<br><button type=\"submit\">OK</button></form>");
@@ -1304,17 +1346,17 @@ void WebCfgServer::buildMqttConfigHtml(String &response)
response.concat("<form class=\"adapt\" method=\"post\" action=\"savecfg\">");
response.concat("<h3>Basic MQTT and Network Configuration</h3>");
response.concat("<table>");
printInputField(response, "HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100);
printInputField(response, "MQTTSERVER", "MQTT Broker", _preferences->getString(preference_mqtt_broker).c_str(), 100);
printInputField(response, "MQTTPORT", "MQTT Broker port", _preferences->getInt(preference_mqtt_broker_port), 5);
printInputField(response, "MQTTUSER", "MQTT User (# to clear)", _preferences->getString(preference_mqtt_user).c_str(), 30, false, true);
printInputField(response, "MQTTPASS", "MQTT Password", "*", 30, true, true);
printInputField(response, "HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100, "");
printInputField(response, "MQTTSERVER", "MQTT Broker", _preferences->getString(preference_mqtt_broker).c_str(), 100, "");
printInputField(response, "MQTTPORT", "MQTT Broker port", _preferences->getInt(preference_mqtt_broker_port), 5, "");
printInputField(response, "MQTTUSER", "MQTT User (# to clear)", _preferences->getString(preference_mqtt_user).c_str(), 30, "", false, true);
printInputField(response, "MQTTPASS", "MQTT Password", "*", 30, "", true, true);
response.concat("</table><br>");
response.concat("<h3>Advanced MQTT and Network Configuration</h3>");
response.concat("<table>");
printInputField(response, "HASSDISCOVERY", "Home Assistant discovery topic (empty to disable; usually homeassistant)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30);
printInputField(response, "HASSCUURL", "Home Assistant device configuration URL (empty to use http://LOCALIP; fill when using a reverse proxy for example)", _preferences->getString(preference_mqtt_hass_cu_url).c_str(), 261);
printInputField(response, "HASSDISCOVERY", "Home Assistant discovery topic (empty to disable; usually homeassistant)", _preferences->getString(preference_mqtt_hass_discovery).c_str(), 30, "");
printInputField(response, "HASSCUURL", "Home Assistant device configuration URL (empty to use http://LOCALIP; fill when using a reverse proxy for example)", _preferences->getString(preference_mqtt_hass_cu_url).c_str(), 261, "");
if(_nukiOpener != nullptr) printCheckBox(response, "OPENERCONT", "Set Nuki Opener Lock/Unlock action in Home Assistant to Continuous mode", _preferences->getBool(preference_opener_continuous_mode), "");
printTextarea(response, "MQTTCA", "MQTT SSL CA Certificate (*, optional)", _preferences->getString(preference_mqtt_ca).c_str(), TLS_CA_MAX_SIZE, _network->encryptionSupported(), true);
printTextarea(response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, _network->encryptionSupported(), true);
@@ -1322,22 +1364,26 @@ void WebCfgServer::buildMqttConfigHtml(String &response)
printDropDown(response, "NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions());
printCheckBox(response, "NWHWWIFIFB", "Disable fallback to Wi-Fi / Wi-Fi config portal", _preferences->getBool(preference_network_wifi_fallback_disabled), "");
printCheckBox(response, "BESTRSSI", "Connect to AP with the best signal in an environment with multiple APs with the same SSID", _preferences->getBool(preference_find_best_rssi), "");
printInputField(response, "RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6);
printInputField(response, "NETTIMEOUT", "Network Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5);
printInputField(response, "RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6, "");
printInputField(response, "NETTIMEOUT", "Network Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5, "");
printCheckBox(response, "RSTDISC", "Restart on disconnect", _preferences->getBool(preference_restart_on_disconnect), "");
printCheckBox(response, "MQTTLOG", "Enable MQTT logging", _preferences->getBool(preference_mqtt_log_enabled), "");
printCheckBox(response, "CHECKUPDATE", "Check for Firmware Updates every 24h", _preferences->getBool(preference_check_updates), "");
printCheckBox(response, "DISNONJSON", "Disable some extraneous non-JSON topics", _preferences->getBool(preference_disable_non_json), "");
printCheckBox(response, "OFFHYBRID", "Enable hybrid official MQTT and Nuki Hub setup", _preferences->getBool(preference_official_hybrid), "");
printCheckBox(response, "HYBRIDACT", "Enable sending actions through official MQTT", _preferences->getBool(preference_official_hybrid_actions), "");
printInputField(response, "HYBRIDTIMER", "Time between status updates when official MQTT is offline (seconds)", _preferences->getInt(preference_query_interval_hybrid_lockstate), 5, "");
printCheckBox(response, "HYBRIDRETRY", "Retry command sent using official MQTT over BLE if failed", _preferences->getBool(preference_official_hybrid_retry), "");
response.concat("</table>");
response.concat("* If no encryption is configured for the MQTT broker, leave empty. Only supported for Wi-Fi connections.<br><br>");
response.concat("<h3>IP Address assignment</h3>");
response.concat("<table>");
printCheckBox(response, "DHCPENA", "Enable DHCP", _preferences->getBool(preference_ip_dhcp_enabled), "");
printInputField(response, "IPADDR", "Static IP address", _preferences->getString(preference_ip_address).c_str(), 15);
printInputField(response, "IPSUB", "Subnet", _preferences->getString(preference_ip_subnet).c_str(), 15);
printInputField(response, "IPGTW", "Default gateway", _preferences->getString(preference_ip_gateway).c_str(), 15);
printInputField(response, "DNSSRV", "DNS Server", _preferences->getString(preference_ip_dns_server).c_str(), 15);
printInputField(response, "IPADDR", "Static IP address", _preferences->getString(preference_ip_address).c_str(), 15, "");
printInputField(response, "IPSUB", "Subnet", _preferences->getString(preference_ip_subnet).c_str(), 15, "");
printInputField(response, "IPGTW", "Default gateway", _preferences->getString(preference_ip_gateway).c_str(), 15, "");
printInputField(response, "DNSSRV", "DNS Server", _preferences->getString(preference_ip_dns_server).c_str(), 15, "");
response.concat("</table>");
response.concat("<br><input type=\"submit\" name=\"submit\" value=\"Save\">");
@@ -1356,18 +1402,20 @@ void WebCfgServer::buildAdvancedConfigHtml(String &response)
response.concat(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Enabled" : "Disabled");
response.concat("</td></tr>");
printCheckBox(response, "BTLPRST", "Enable Bootloop prevention (Try to reset these settings to default on bootloop)", true, "");
printInputField(response, "BUFFSIZE", "Char buffer size (min 4096, max 32768)", _preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE), 6);
printInputField(response, "TSKNTWK", "Task size Network (min 12288, max 32768)", _preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE), 6);
printInputField(response, "TSKNUKI", "Task size Nuki (min 8192, max 32768)", _preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), 6);
printInputField(response, "TSKPD", "Task size Presence Detection (min 1024, max 4048)", _preferences->getInt(preference_task_size_pd, PD_TASK_SIZE), 6);
printInputField(response, "ALMAX", "Max auth log entries (min 1, max 50)", _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 3);
printInputField(response, "KPMAX", "Max keypad entries (min 1, max 100)", _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD), 3);
printInputField(response, "TCMAX", "Max timecontrol entries (min 1, max 50)", _preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL), 3);
printInputField(response, "BUFFSIZE", "Char buffer size (min 4096, max 32768)", _preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE), 6, "");
printInputField(response, "TSKNTWK", "Task size Network (min 12288, max 32768)", _preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE), 6, "");
printInputField(response, "TSKNUKI", "Task size Nuki (min 8192, max 32768)", _preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), 6, "");
printInputField(response, "TSKPD", "Task size Presence Detection (min 1024, max 4048)", _preferences->getInt(preference_task_size_pd, PD_TASK_SIZE), 6, "");
printInputField(response, "ALMAX", "Max auth log entries (min 1, max 50)", _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 3, "inputmaxauthlog");
printInputField(response, "KPMAX", "Max keypad entries (min 1, max 100)", _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD), 3, "inputmaxkeypad");
printInputField(response, "TCMAX", "Max timecontrol entries (min 1, max 50)", _preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL), 3, "inputmaxtimecontrol");
response.concat("<tr><td>Advised minimum char buffer size based on current settings</td><td id=\"mincharbuffer\"></td>");
response.concat("<tr><td>Advised minimum network task size based on current settings</td><td id=\"minnetworktask\"></td>");
response.concat("</table>");
response.concat("<br><input type=\"submit\" name=\"submit\" value=\"Save\">");
response.concat("</form>");
response.concat("</body></html>");
response.concat("</body><script>window.onload=function(){ document.getElementById(\"inputmaxauthlog\").addEventListener(\"keyup\", calculate);document.getElementById(\"inputmaxkeypad\").addEventListener(\"keyup\", calculate);document.getElementById(\"inputmaxtimecontrol\").addEventListener(\"keyup\", calculate); calculate(); }; function calculate() { var authlog = document.getElementById(\"inputmaxauthlog\").value; var keypad = document.getElementById(\"inputmaxkeypad\").value; var timecontrol = document.getElementById(\"inputmaxtimecontrol\").value; var charbuf = 0; var networktask = 0; var sizeauthlog = 0; var sizekeypad = 0; var sizetimecontrol = 0; if(authlog > 0) { sizeauthlog = 280 * authlog; } if(keypad > 0) { sizekeypad = 350 * keypad; } if(timecontrol > 0) { sizetimecontrol = 120 * timecontrol; } charbuf = sizetimecontrol; networktask = 10240 + sizetimecontrol; if(sizeauthlog>sizekeypad && sizeauthlog>sizetimecontrol) { charbuf = sizeauthlog; networktask = 10240 + sizeauthlog;} else if(sizekeypad>sizeauthlog && sizekeypad>sizetimecontrol) { charbuf = sizekeypad; networktask = 10240 + sizekeypad;} if(charbuf<4096) { charbuf = 4096; } else if (charbuf>32768) { charbuf = 32768; } if(networktask<12288) { networktask = 12288; } else if (networktask>32768) { networktask = 32768; } document.getElementById(\"mincharbuffer\").innerHTML = charbuf; document.getElementById(\"minnetworktask\").innerHTML = networktask; }</script></html>");
}
void WebCfgServer::buildStatusHtml(String &response)
@@ -1400,7 +1448,7 @@ void WebCfgServer::buildStatusHtml(String &response)
if(_nuki->isPaired())
{
json["lockPin"] = pinStateToString(_preferences->getInt(preference_lock_pin_status, 4));
lockDone = true;
if(strcmp(lockStateArr, "undefined") != 0) lockDone = true;
}
else json["lockPin"] = "Not Paired";
}
@@ -1419,7 +1467,7 @@ void WebCfgServer::buildStatusHtml(String &response)
if(_nukiOpener->isPaired())
{
json["openerPin"] = pinStateToString(_preferences->getInt(preference_opener_pin_status, 4));
openerDone = true;
if(strcmp(openerStateArr, "undefined") != 0) openerDone = true;
}
else json["openerPin"] = "Not Paired";
}
@@ -1462,14 +1510,17 @@ void WebCfgServer::buildAccLvlHtml(String &response)
response.concat("<input type=\"hidden\" name=\"ACLLVLCHANGED\" value=\"1\">");
response.concat("<h3>Nuki General Access Control</h3>");
response.concat("<table><tr><th>Setting</th><th>Enabled</th></tr>");
printCheckBox(response, "CONFPUB", "Publish Nuki configuration information", _preferences->getBool(preference_conf_info_enabled, true), "");
if((_nuki != nullptr && _nuki->hasKeypad()) || (_nukiOpener != nullptr && _nukiOpener->hasKeypad()))
{
printCheckBox(response, "KPPUB", "Publish keypad entries information", _preferences->getBool(preference_keypad_info_enabled), "");
printCheckBox(response, "KPPER", "Publish a topic per keypad entry and create HA sensor", _preferences->getBool(preference_keypad_topic_per_entry), "");
printCheckBox(response, "KPCODE", "Also publish keypad codes (<span class=\"warning\">Disadvised for security reasons</span>)", _preferences->getBool(preference_keypad_publish_code, false), "");
printCheckBox(response, "KPENA", "Add, modify and delete keypad codes", _preferences->getBool(preference_keypad_control_enabled), "");
}
printCheckBox(response, "TCPUB", "Publish time control entries information", _preferences->getBool(preference_timecontrol_info_enabled), "");
printCheckBox(response, "TCPER", "Publish a topic per time control entry and create HA sensor", _preferences->getBool(preference_timecontrol_topic_per_entry), "");
printCheckBox(response, "TCENA", "Add, modify and delete time control entries", _preferences->getBool(preference_timecontrol_control_enabled), "");
printCheckBox(response, "PUBAUTH", "Publish authorization log (may reduce battery life)", _preferences->getBool(preference_publish_authdata), "");
response.concat("</table><br>");
@@ -1548,9 +1599,9 @@ void WebCfgServer::buildAccLvlHtml(String &response)
}
if(_nukiOpener != nullptr)
{
uint32_t basicOpenerConfigAclPrefs[16];
uint32_t basicOpenerConfigAclPrefs[14];
_preferences->getBytes(preference_conf_opener_basic_acl, &basicOpenerConfigAclPrefs, sizeof(basicOpenerConfigAclPrefs));
uint32_t advancedOpenerConfigAclPrefs[22];
uint32_t advancedOpenerConfigAclPrefs[20];
_preferences->getBytes(preference_conf_opener_advanced_acl, &advancedOpenerConfigAclPrefs, sizeof(advancedOpenerConfigAclPrefs));
response.concat("<h3>Nuki Opener Access Control</h3>");
@@ -1630,32 +1681,33 @@ void WebCfgServer::buildNukiConfigHtml(String &response)
if(_preferences->getBool(preference_lock_enabled))
{
printInputField(response, "MQTTPATH", "MQTT Nuki Smartlock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180);
printInputField(response, "MQTTPATH", "MQTT Nuki Smartlock Path", _preferences->getString(preference_mqtt_lock_path).c_str(), 180, "");
}
printCheckBox(response, "OPENA", "Nuki Opener enabled", _preferences->getBool(preference_opener_enabled), "");
if(_preferences->getBool(preference_opener_enabled))
{
printInputField(response, "MQTTOPPATH", "MQTT Nuki Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180);
printInputField(response, "MQTTOPPATH", "MQTT Nuki Opener Path", _preferences->getString(preference_mqtt_opener_path).c_str(), 180, "");
}
response.concat("</table><br>");
response.concat("<h3>Advanced Nuki Configuration</h3>");
response.concat("<table>");
printInputField(response, "LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10);
printInputField(response, "CFGINT", "Query interval configuration (seconds)", _preferences->getInt(preference_query_interval_configuration), 10);
printInputField(response, "BATINT", "Query interval battery (seconds)", _preferences->getInt(preference_query_interval_battery), 10);
printInputField(response, "LSTINT", "Query interval lock state (seconds)", _preferences->getInt(preference_query_interval_lockstate), 10, "");
printInputField(response, "CFGINT", "Query interval configuration (seconds)", _preferences->getInt(preference_query_interval_configuration), 10, "");
printInputField(response, "BATINT", "Query interval battery (seconds)", _preferences->getInt(preference_query_interval_battery), 10, "");
if((_nuki != nullptr && _nuki->hasKeypad()) || (_nukiOpener != nullptr && _nukiOpener->hasKeypad()))
{
printInputField(response, "KPINT", "Query interval keypad (seconds)", _preferences->getInt(preference_query_interval_keypad), 10);
printInputField(response, "KPINT", "Query interval keypad (seconds)", _preferences->getInt(preference_query_interval_keypad), 10, "");
}
printInputField(response, "NRTRY", "Number of retries if command failed", _preferences->getInt(preference_command_nr_of_retries), 10);
printInputField(response, "TRYDLY", "Delay between retries (milliseconds)", _preferences->getInt(preference_command_retry_delay), 10);
printCheckBox(response, "REGAPP", "Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_as_app), "");
printInputField(response, "PRDTMO", "Presence detection timeout (seconds; -1 to disable)", _preferences->getInt(preference_presence_detection_timeout), 10);
printInputField(response, "RSBC", "Restart if bluetooth beacons not received (seconds; -1 to disable)", _preferences->getInt(preference_restart_ble_beacon_lost), 10);
printInputField(response, "NRTRY", "Number of retries if command failed", _preferences->getInt(preference_command_nr_of_retries), 10, "");
printInputField(response, "TRYDLY", "Delay between retries (milliseconds)", _preferences->getInt(preference_command_retry_delay), 10, "");
if(_nuki != nullptr) printCheckBox(response, "REGAPP", "Lock: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_as_app), "");
if(_nukiOpener != nullptr) printCheckBox(response, "REGAPPOPN", "Opener: Nuki Bridge is running alongside Nuki Hub (needs re-pairing if changed)", _preferences->getBool(preference_register_opener_as_app), "");
printInputField(response, "PRDTMO", "Presence detection timeout (seconds; -1 to disable)", _preferences->getInt(preference_presence_detection_timeout), 10, "");
printInputField(response, "RSBC", "Restart if bluetooth beacons not received (seconds; -1 to disable)", _preferences->getInt(preference_restart_ble_beacon_lost), 10, "");
response.concat("</table>");
response.concat("<br><input type=\"submit\" name=\"submit\" value=\"Save\">");
response.concat("</form>");
@@ -1845,9 +1897,9 @@ void WebCfgServer::buildInfoHtml(String &response)
if(_nukiOpener != nullptr)
{
uint32_t basicOpenerConfigAclPrefs[16];
uint32_t basicOpenerConfigAclPrefs[14];
_preferences->getBytes(preference_conf_opener_basic_acl, &basicOpenerConfigAclPrefs, sizeof(basicOpenerConfigAclPrefs));
uint32_t advancedOpenerConfigAclPrefs[22];
uint32_t advancedOpenerConfigAclPrefs[20];
_preferences->getBytes(preference_conf_opener_advanced_acl, &advancedOpenerConfigAclPrefs, sizeof(advancedOpenerConfigAclPrefs));
response.concat("Opener firmware version: ");
response.concat(_nukiOpener->firmwareVersion().c_str());
@@ -2105,6 +2157,7 @@ void WebCfgServer::printInputField(String& response,
const char *description,
const char *value,
const size_t& maxLength,
const char *id,
const bool& isPassword,
const bool& showLengthRestriction)
{
@@ -2124,7 +2177,13 @@ void WebCfgServer::printInputField(String& response,
response.concat("</td><td>");
response.concat("<input type=");
response.concat(isPassword ? "password" : "text");
response.concat(isPassword ? "\"password\"" : "\"text\"");
if(id)
{
response.concat(" id=\"");
response.concat(id);
response.concat("\"");
}
response.concat(" value=\"");
response.concat(value);
response.concat("\" name=\"");
@@ -2139,11 +2198,12 @@ void WebCfgServer::printInputField(String& response,
const char *token,
const char *description,
const int value,
size_t maxLength)
size_t maxLength,
const char *id)
{
char valueStr[20];
itoa(value, valueStr, 10);
printInputField(response, token, description, valueStr, maxLength);
printInputField(response, token, description, valueStr, maxLength, id);
}
void WebCfgServer::printCheckBox(String &response, const char *token, const char *description, const bool value, const char *htmlClass)

View File

@@ -53,10 +53,9 @@ private:
void sendFavicon();
void processUnpair(bool opener);
void processFactoryReset();
void buildHtmlHeader(String& response, String additionalHeader = "");
void printInputField(String& response, const char* token, const char* description, const char* value, const size_t& maxLength, const bool& isPassword = false, const bool& showLengthRestriction = false);
void printInputField(String& response, const char* token, const char* description, const int value, size_t maxLength);
void printInputField(String& response, const char* token, const char* description, const char* value, const size_t& maxLength, const char* id, const bool& isPassword = false, const bool& showLengthRestriction = false);
void printInputField(String& response, const char* token, const char* description, const int value, size_t maxLength, const char* id);
void printCheckBox(String& response, const char* token, const char* description, const bool value, const char* htmlClass);
void printTextarea(String& response, const char *token, const char *description, const char *value, const size_t& maxLength, const bool& enabled = true, const bool& showLengthRestriction = false);
void printDropDown(String &response, const char *token, const char *description, const String preselectedValue, std::vector<std::pair<String, String>> options);

View File

@@ -154,6 +154,7 @@ bool initPreferences()
{
preferences->putBool(preference_started_before, true);
preferences->putBool(preference_lock_enabled, true);
preferences->putBool(preference_conf_info_enabled, true);
uint32_t aclPrefs[17] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
preferences->putBytes(preference_acl, (byte*)(&aclPrefs), sizeof(aclPrefs));
uint32_t basicLockConfigAclPrefs[16] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};

View File

@@ -31,7 +31,7 @@ public:
virtual bool isConnected() = 0;
virtual int8_t signalStrength() = 0;
virtual String localIP() = 0;
virtual String BSSIDstr() = 0;