This commit is contained in:
iranl
2024-05-11 13:50:54 +02:00
parent 29d57dd621
commit f6d2eda3cb
51 changed files with 98 additions and 6 deletions

13
src/CharBuffer.cpp Normal file
View File

@@ -0,0 +1,13 @@
#include "CharBuffer.h"
void CharBuffer::initialize()
{
_buffer = new char[CHAR_BUFFER_SIZE];
}
char *CharBuffer::get()
{
return _buffer;
}
char* CharBuffer::_buffer;

13
src/CharBuffer.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#define CHAR_BUFFER_SIZE 4096
class CharBuffer
{
public:
static void initialize();
static char* get();
private:
static char* _buffer;
};

12
src/Config.h Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#define NUKI_HUB_VERSION "8.33"
#define GITHUB_LATEST_RELEASE_URL "https://github.com/technyon/nuki_hub/releases/latest"
#define GITHUB_LATEST_RELEASE_API_URL "https://api.github.com/repos/technyon/nuki_hub/releases/latest"
#define GITHUB_LATEST_RELEASE_BINARY_URL "https://github.com/technyon/nuki_hub/raw/master/webflash/nuki_hub.bin"
#define MQTT_QOS_LEVEL 1
#define MQTT_CLEAN_SESSIONS false
#define GPIO_DEBOUNCE_TIME 200

388
src/Gpio.cpp Normal file
View File

@@ -0,0 +1,388 @@
#include <esp32-hal.h>
#include "Gpio.h"
#include "Config.h"
#include "Arduino.h"
#include "Logger.h"
#include "PreferencesKeys.h"
#include "RestartReason.h"
#include "lib/gpio2go/src/Gpio2Go.h"
Gpio* Gpio::_inst = nullptr;
unsigned long Gpio::_debounceTs = 0;
const uint Gpio::_debounceTime = GPIO_DEBOUNCE_TIME;
Gpio::Gpio(Preferences* preferences)
: _preferences(preferences)
{
_inst = this;
loadPinConfiguration();
if(_preferences->getBool(preference_gpio_locking_enabled))
{
migrateObsoleteSetting();
}
_inst->init();
}
void Gpio::init()
{
for(const auto& entry : _inst->_pinConfiguration)
{
const auto it = std::find(_inst->availablePins().begin(), _inst->availablePins().end(), entry.pin);
if(it == _inst->availablePins().end())
{
continue;
}
switch(entry.role)
{
case PinRole::InputLock:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrLock, FALLING);
break;
case PinRole::InputUnlock:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrUnlock, FALLING);
break;
case PinRole::InputUnlatch:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrUnlatch, FALLING);
break;
case PinRole::InputLockNgo:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrLockNgo, FALLING);
break;
case PinRole::InputLockNgoUnlatch:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrLockNgoUnlatch, FALLING);
break;
case PinRole::InputElectricStrikeActuation:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrElectricStrikeActuation, FALLING);
break;
case PinRole::InputActivateRTO:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrActivateRTO, FALLING);
break;
case PinRole::InputActivateCM:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrActivateCM, FALLING);
break;
case PinRole::InputDeactivateRtoCm:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrDeactivateRtoCm, FALLING);
break;
case PinRole::InputDeactivateRTO:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrDeactivateRTO, FALLING);
break;
case PinRole::InputDeactivateCM:
pinMode(entry.pin, INPUT_PULLUP);
attachInterrupt(entry.pin, isrDeactivateCM, FALLING);
break;
case PinRole::OutputHighLocked:
case PinRole::OutputHighUnlocked:
case PinRole::OutputHighMotorBlocked:
case PinRole::OutputHighRtoActive:
case PinRole::OutputHighCmActive:
case PinRole::OutputHighRtoOrCmActive:
case PinRole::GeneralOutput:
pinMode(entry.pin, OUTPUT);
break;
case PinRole::GeneralInputPullDown:
Gpio2Go::configurePin(entry.pin, PinMode::InputPullDown, InterruptMode::Change, 300);
break;
case PinRole::GeneralInputPullUp:
Gpio2Go::configurePin(entry.pin, PinMode::InputPullup, InterruptMode::Change, 300);
break;
default:
pinMode(entry.pin, OUTPUT);
break;
}
Gpio2Go::subscribe(Gpio::inputCallback);
}
}
const std::vector<uint8_t>& Gpio::availablePins() const
{
return _availablePins;
}
void Gpio::loadPinConfiguration()
{
size_t storedLength = _preferences->getBytesLength(preference_gpio_configuration);
if(storedLength == 0)
{
return;
}
uint8_t serialized[storedLength];
memset(serialized, 0, sizeof(serialized));
size_t size = _preferences->getBytes(preference_gpio_configuration, serialized, sizeof(serialized));
if(size == 0)
{
return;
}
size_t numEntries = size / 2;
_pinConfiguration.clear();
_pinConfiguration.reserve(numEntries);
for(int i=0; i < numEntries; i++)
{
PinEntry entry;
entry.pin = serialized[i * 2];
entry.role = (PinRole) serialized[(i * 2 + 1)];
if(entry.role != PinRole::Disabled)
{
_pinConfiguration.push_back(entry);
}
}
}
void Gpio::savePinConfiguration(const std::vector<PinEntry> &pinConfiguration)
{
int8_t serialized[std::max(pinConfiguration.size() * 2, _preferences->getBytesLength(preference_gpio_configuration))];
memset(serialized, 0, sizeof(serialized));
int len = pinConfiguration.size();
for(int i=0; i < len; i++)
{
const auto& entry = pinConfiguration[i];
if(entry.role != PinRole::Disabled)
{
serialized[i * 2] = entry.pin;
serialized[i * 2 + 1] = (int8_t) entry.role;
}
}
_preferences->putBytes(preference_gpio_configuration, serialized, sizeof(serialized));
}
const std::vector<PinEntry> &Gpio::pinConfiguration() const
{
return _pinConfiguration;
}
const PinRole Gpio::getPinRole(const int &pin) const
{
for(const auto& pinEntry : _pinConfiguration)
{
if(pinEntry.pin == pin)
{
return pinEntry.role;
}
}
return PinRole::Disabled;
}
String Gpio::getRoleDescription(PinRole role) const
{
switch(role)
{
case PinRole::Disabled:
return "Disabled";
case PinRole::InputLock:
return "Input: Lock";
case PinRole::InputUnlock:
return "Input: Unlock";
case PinRole::InputUnlatch:
return "Input: Unlatch";
case PinRole::InputLockNgo:
return "Input: Lock n Go";
case PinRole::InputLockNgoUnlatch:
return "Input: Lock n Go and unlatch";
case PinRole::InputElectricStrikeActuation:
return "Input: Electric strike actuation";
case PinRole::InputActivateRTO:
return "Input: Activate RTO";
case PinRole::InputActivateCM:
return "Input: Activate CM";
case PinRole::InputDeactivateRtoCm:
return "Input: Deactivate RTO/CM";
case PinRole::InputDeactivateRTO:
return "Input: Deactivate RTO";
case PinRole::InputDeactivateCM:
return "Input: Deactivate CM";
case PinRole::OutputHighLocked:
return "Output: High when locked";
case PinRole::OutputHighUnlocked:
return "Output: High when unlocked";
case PinRole::OutputHighMotorBlocked:
return "Output: High when motor blocked";
case PinRole::OutputHighRtoActive:
return "Output: High when RTO active";
case PinRole::OutputHighCmActive:
return "Output: High when CM active";
case PinRole::OutputHighRtoOrCmActive:
return "Output: High when RTO or CM active";
case PinRole::GeneralOutput:
return "General output";
case PinRole::GeneralInputPullDown:
return "General input (Pull-down)";
case PinRole::GeneralInputPullUp:
return "General input (Pull-up)";
default:
return "Unknown";
}
}
void Gpio::getConfigurationText(String& text, const std::vector<PinEntry>& pinConfiguration, const String& linebreak) const
{
for(const auto& entry : pinConfiguration)
{
if(entry.role != PinRole::Disabled)
{
text.concat("GPIO ");
text.concat(entry.pin);
if(entry.pin < 10)
{
text.concat(' ');
}
text.concat(": ");
text.concat(getRoleDescription(entry.role));
text.concat(linebreak);
}
}
}
const std::vector<PinRole>& Gpio::getAllRoles() const
{
return _allRoles;
}
void Gpio::notify(const GpioAction &action, const int& pin)
{
for(auto& callback : _callbacks)
{
callback(action, pin);
}
}
void Gpio::inputCallback(const int &pin)
{
_inst->notify(GpioAction::GeneralInput, pin);
}
void Gpio::addCallback(std::function<void(const GpioAction&, const int&)> callback)
{
_callbacks.push_back(callback);
}
void Gpio::isrLock()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::Lock, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrUnlock()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::Unlock, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrUnlatch()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::Unlatch, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrLockNgo()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::LockNgo, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrLockNgoUnlatch()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::LockNgoUnlatch, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrElectricStrikeActuation()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::ElectricStrikeActuation, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrActivateRTO()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::ActivateRTO, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrActivateCM()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::ActivateCM, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrDeactivateRtoCm()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::DeactivateRtoCm, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrDeactivateRTO()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::DeactivateRTO, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::isrDeactivateCM()
{
if(millis() < _debounceTs) return;
_inst->notify(GpioAction::DeactivateCM, -1);
_debounceTs = millis() + _debounceTime;
}
void Gpio::setPinOutput(const uint8_t& pin, const uint8_t& state)
{
digitalWrite(pin, state);
}
void Gpio::migrateObsoleteSetting()
{
_pinConfiguration.clear();
PinEntry entry1;
entry1.pin = 27;
entry1.role = PinRole::InputUnlatch;
PinEntry entry2;
entry2.pin = 32;
entry2.role = PinRole::InputLock;
PinEntry entry3;
entry3.pin = 33;
entry3.role = PinRole::InputUnlock;
_pinConfiguration.push_back(entry1);
_pinConfiguration.push_back(entry2);
_pinConfiguration.push_back(entry3);
savePinConfiguration(_pinConfiguration);
_preferences->remove(preference_gpio_locking_enabled);
Log->println("Migrated gpio control setting");
delay(200);
restartEsp(RestartReason::GpioConfigurationUpdated);
}

128
src/Gpio.h Normal file
View File

@@ -0,0 +1,128 @@
#pragma once
#include <functional>
#include <Preferences.h>
#include <vector>
enum class PinRole
{
Disabled,
InputLock,
InputUnlock,
InputUnlatch,
InputLockNgo,
InputLockNgoUnlatch,
InputElectricStrikeActuation,
InputActivateRTO,
InputActivateCM,
InputDeactivateRtoCm,
InputDeactivateRTO,
InputDeactivateCM,
OutputHighLocked,
OutputHighUnlocked,
OutputHighMotorBlocked,
OutputHighRtoActive,
OutputHighCmActive,
OutputHighRtoOrCmActive,
GeneralOutput,
GeneralInputPullDown,
GeneralInputPullUp
};
enum class GpioAction
{
Lock,
Unlock,
Unlatch,
LockNgo,
LockNgoUnlatch,
ElectricStrikeActuation,
ActivateRTO,
ActivateCM,
DeactivateRtoCm,
DeactivateRTO,
DeactivateCM,
GeneralInput
};
struct PinEntry
{
uint8_t pin = 0;
PinRole role = PinRole::Disabled;
};
class Gpio
{
public:
Gpio(Preferences* preferences);
static void init();
void migrateObsoleteSetting();
void addCallback(std::function<void(const GpioAction&, const int&)> callback);
void loadPinConfiguration();
void savePinConfiguration(const std::vector<PinEntry>& pinConfiguration);
const std::vector<uint8_t>& availablePins() const;
const std::vector<PinEntry>& pinConfiguration() const;
const PinRole getPinRole(const int& pin) const;
String getRoleDescription(PinRole role) const;
void getConfigurationText(String& text, const std::vector<PinEntry>& pinConfiguration, const String& linebreak = "\n") const;
const std::vector<PinRole>& getAllRoles() const;
void setPinOutput(const uint8_t& pin, const uint8_t& state);
private:
void IRAM_ATTR notify(const GpioAction& action, const int& pin);
static void inputCallback(const int & pin);
const std::vector<uint8_t> _availablePins = { 2, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 32, 33 };
const std::vector<PinRole> _allRoles =
{
PinRole::Disabled,
PinRole::InputLock,
PinRole::InputUnlock,
PinRole::InputUnlatch,
PinRole::InputLockNgo,
PinRole::InputLockNgoUnlatch,
PinRole::InputElectricStrikeActuation,
PinRole::InputActivateRTO,
PinRole::InputActivateCM,
PinRole::InputDeactivateRtoCm,
PinRole::InputDeactivateRTO,
PinRole::InputDeactivateCM,
PinRole::OutputHighLocked,
PinRole::OutputHighUnlocked,
PinRole::OutputHighRtoActive,
PinRole::OutputHighCmActive,
PinRole::OutputHighRtoOrCmActive,
PinRole::GeneralInputPullDown,
PinRole::GeneralInputPullUp,
PinRole::GeneralOutput
};
std::vector<PinEntry> _pinConfiguration;
static const uint _debounceTime;
static void IRAM_ATTR isrLock();
static void IRAM_ATTR isrUnlock();
static void IRAM_ATTR isrUnlatch();
static void IRAM_ATTR isrLockNgo();
static void IRAM_ATTR isrLockNgoUnlatch();
static void IRAM_ATTR isrElectricStrikeActuation();
static void IRAM_ATTR isrActivateRTO();
static void IRAM_ATTR isrActivateCM();
static void IRAM_ATTR isrDeactivateRtoCm();
static void IRAM_ATTR isrDeactivateRTO();
static void IRAM_ATTR isrDeactivateCM();
std::vector<std::function<void(const GpioAction&, const int&)>> _callbacks;
static Gpio* _inst;
static unsigned long _debounceTs;
Preferences* _preferences = nullptr;
};

9
src/LockActionResult.h Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
enum class LockActionResult
{
Success,
UnknownAction,
AccessDenied,
Failed
};

3
src/Logger.cpp Normal file
View File

@@ -0,0 +1,3 @@
#include "Logger.h"
Print* Log = nullptr;

7
src/Logger.h Normal file
View File

@@ -0,0 +1,7 @@
#ifndef MQTT_LOGGER_GLOBAL
#define MQTT_LOGGER_GLOBAL
#include "MqttLogger.h"
extern Print* Log;
#endif

9
src/MqttReceiver.h Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
#include <Arduino.h>
class MqttReceiver
{
public:
virtual void onMqttDataReceived(const char* topic, byte* payload, const unsigned int length) = 0;
};

82
src/MqttTopics.h Normal file
View File

@@ -0,0 +1,82 @@
#pragma once
#define mqtt_topic_lock_action "/lock/action"
#define mqtt_topic_lock_state "/lock/state"
#define mqtt_topic_lock_ha_state "/lock/hastate"
#define mqtt_topic_lock_json "/lock/json"
#define mqtt_topic_lock_binary_state "/lock/binaryState"
#define mqtt_topic_lock_continuous_mode "/lock/continuousMode"
#define mqtt_topic_lock_ring "/lock/ring"
#define mqtt_topic_lock_binary_ring "/lock/binaryRing"
#define mqtt_topic_lock_trigger "/lock/trigger"
#define mqtt_topic_lock_last_lock_action "/lock/lastLockAction"
#define mqtt_topic_lock_log "/lock/log"
#define mqtt_topic_lock_auth_id "/lock/authorizationId"
#define mqtt_topic_lock_auth_name "/lock/authorizationName"
#define mqtt_topic_lock_completionStatus "/lock/completionStatus"
#define mqtt_topic_lock_action_command_result "/lock/commandResult"
#define mqtt_topic_lock_door_sensor_state "/lock/doorSensorState"
#define mqtt_topic_lock_rssi "/lock/rssi"
#define mqtt_topic_lock_address "/lock/address"
#define mqtt_topic_lock_retry "/lock/retry"
#define mqtt_topic_config_button_enabled "/configuration/buttonEnabled"
#define mqtt_topic_config_led_enabled "/configuration/ledEnabled"
#define mqtt_topic_config_led_brightness "/configuration/ledBrightness"
#define mqtt_topic_config_auto_unlock "/configuration/autoUnlock"
#define mqtt_topic_config_auto_lock "/configuration/autoLock"
#define mqtt_topic_config_single_lock "/configuration/singleLock"
#define mqtt_topic_config_sound_level "/configuration/soundLevel"
#define mqtt_topic_query_config "/lock/query/config"
#define mqtt_topic_query_lockstate "/lock/query/lockstate"
#define mqtt_topic_query_keypad "/lock/query/keypad"
#define mqtt_topic_query_battery "/lock/query/battery"
#define mqtt_topic_query_lockstate_command_result "/lock/query/lockstateCommandResult"
#define mqtt_topic_battery_level "/battery/level"
#define mqtt_topic_battery_critical "/battery/critical"
#define mqtt_topic_battery_charging "/battery/charging"
#define mqtt_topic_battery_voltage "/battery/voltage"
#define mqtt_topic_battery_drain "/battery/drain"
#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_keypad "/keypad"
#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"
#define mqtt_topic_keypad_command_code "/keypad/command/code"
#define mqtt_topic_keypad_command_enabled "/keypad/command/enabled"
#define mqtt_topic_keypad_command_result "/keypad/command/commandResult"
#define mqtt_topic_keypad_json "/keypad/json"
#define mqtt_topic_keypad_json_action "/keypad/actionJson"
#define mqtt_topic_keypad_json_command_result "/keypad/commandResultJson"
#define mqtt_topic_timecontrol_json "/timecontrol/json"
#define mqtt_topic_timecontrol_action "/timecontrol/action"
#define mqtt_topic_timecontrol_command_result "/timecontrol/commandResult"
#define mqtt_topic_info_hardware_version "/info/hardwareVersion"
#define mqtt_topic_info_firmware_version "/info/firmwareVersion"
#define mqtt_topic_info_nuki_hub_version "/info/nukiHubVersion"
#define mqtt_topic_info_nuki_hub_latest "/info/nukiHubLatest"
#define mqtt_topic_info_nuki_hub_ip "/info/nukiHubIp"
#define mqtt_topic_reset "/maintenance/reset"
#define mqtt_topic_uptime "/maintenance/uptime"
#define mqtt_topic_wifi_rssi "/maintenance/wifiRssi"
#define mqtt_topic_log "/maintenance/log"
#define mqtt_topic_freeheap "/maintenance/freeHeap"
#define mqtt_topic_restart_reason_fw "/maintenance/restartReasonNukiHub"
#define mqtt_topic_restart_reason_esp "/maintenance/restartReasonNukiEsp"
#define mqtt_topic_mqtt_connection_state "/maintenance/mqttConnectionState"
#define mqtt_topic_network_device "/maintenance/networkDevice"
#define mqtt_topic_presence "/presence/devices"
#define mqtt_topic_gpio_prefix "/gpio"
#define mqtt_topic_gpio_pin "/pin_"
#define mqtt_topic_gpio_role "/role"
#define mqtt_topic_gpio_state "/state"

1749
src/Network.cpp Normal file

File diff suppressed because it is too large Load Diff

177
src/Network.h Normal file
View File

@@ -0,0 +1,177 @@
#pragma once
#include <Preferences.h>
#include <vector>
#include <map>
#include "networkDevices/NetworkDevice.h"
#include "MqttReceiver.h"
#include "networkDevices/IPConfiguration.h"
#include "MqttTopics.h"
#include "Gpio.h"
#include <ArduinoJson.h>
#include <HTTPClient.h>
enum class NetworkDeviceType
{
WiFi,
W5500,
Olimex_LAN8720,
WT32_LAN8720,
M5STACK_PoESP32_Unit,
LilyGO_T_ETH_POE
};
#define JSON_BUFFER_SIZE 1024
class Network
{
public:
explicit Network(Preferences* preferences, Gpio* gpio, const String& maintenancePathPrefix, char* buffer, size_t bufferSize);
void initialize();
bool update();
void registerMqttReceiver(MqttReceiver* receiver);
void reconfigureDevice();
void setMqttPresencePath(char* path);
void disableAutoRestarts(); // disable on OTA start
void disableMqtt();
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 publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, const char* availabilityTopic, const bool& hasKeypad, char* lockAction, char* unlockAction, char* openAction);
void publishHASSConfigAdditionalButtons(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSConfigBatLevel(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSConfigDoorSensor(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSConfigRingDetect(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSConfigContinuousMode(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSConfigLedBrightness(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSConfigSoundLevel(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSConfigAccessLog(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSConfigKeypadAttemptInfo(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSWifiRssiConfig(char* deviceType, const char* baseTopic, char* name, char* uidString);
void publishHASSBleRssiConfig(char* deviceType, const char* baseTopic, char* name, char* uidString);
void removeHASSConfig(char* uidString);
void removeHASSConfigTopic(char* deviceType, char* name, char* uidString);
void clearWifiFallback();
void publishPresenceDetection(char* csv);
int mqttConnectionState(); // 0 = not connected; 1 = connected; 2 = connected and mqtt processed
bool encryptionSupported();
const String networkDeviceName() const;
const String networkBSSID() const;
const NetworkDeviceType networkDeviceType();
uint16_t subscribe(const char* topic, uint8_t qos);
void setKeepAliveCallback(std::function<void()> reconnectTick);
void addReconnectedCallback(std::function<void()> reconnectedCallback);
NetworkDevice* device();
private:
static void onMqttDataReceivedCallback(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
void onMqttDataReceived(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t& len, size_t& index, size_t& total);
void parseGpioTopics(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t& len, size_t& index, size_t& total);
void gpioActionCallback(const GpioAction& action, const int& pin);
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,
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 onMqttConnect(const bool& sessionPresent);
void onMqttDisconnect(const espMqttClientTypes::DisconnectReason& reason);
void buildMqttPath(char* outPath, std::initializer_list<const char*> paths);
static Network* _inst;
const char* _lastWillPayload = "offline";
char _mqttConnectionStateTopic[211] = {0};
String _lockPath;
const char* _latestVersion;
HTTPClient https;
Preferences* _preferences;
Gpio* _gpio;
IPConfiguration* _ipConfiguration = nullptr;
String _hostname;
char _hostnameArr[101] = {0};
NetworkDevice* _device = nullptr;
int _mqttConnectionState = 0;
bool _connectReplyReceived = false;
unsigned long _nextReconnect = 0;
char _mqttBrokerAddr[101] = {0};
char _mqttUser[31] = {0};
char _mqttPass[31] = {0};
char _mqttPresencePrefix[181] = {0};
char _maintenancePathPrefix[181] = {0};
int _networkTimeout = 0;
std::vector<MqttReceiver*> _mqttReceivers;
char* _presenceCsv = nullptr;
bool _restartOnDisconnect = false;
bool _firstConnect = true;
bool _publishDebugInfo = false;
std::vector<String> _subscribedTopics;
std::map<String, String> _initTopics;
unsigned long _lastConnectedTs = 0;
unsigned long _lastMaintenanceTs = 0;
unsigned long _lastUpdateCheckTs = 0;
unsigned long _lastRssiTs = 0;
bool _mqttEnabled = true;
static unsigned long _ignoreSubscriptionsTs;
long _rssiPublishInterval = 0;
std::map<uint8_t, unsigned long> _gpioTs;
char* _buffer;
const size_t _bufferSize;
std::function<void()> _keepAliveCallback = nullptr;
std::vector<std::function<void()>> _reconnectedCallbacks;
NetworkDeviceType _networkDeviceType = (NetworkDeviceType)-1;
int8_t _lastRssi = 127;
};

905
src/NetworkLock.cpp Normal file
View File

@@ -0,0 +1,905 @@
#include "NetworkLock.h"
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager
#include "Arduino.h"
#include "MqttTopics.h"
#include "PreferencesKeys.h"
#include "Logger.h"
#include "RestartReason.h"
#include <ArduinoJson.h>
NetworkLock::NetworkLock(Network* network, Preferences* preferences, char* buffer, size_t bufferSize)
: _network(network),
_preferences(preferences),
_buffer(buffer),
_bufferSize(bufferSize)
{
_configTopics.reserve(5);
_configTopics.push_back(mqtt_topic_config_button_enabled);
_configTopics.push_back(mqtt_topic_config_led_enabled);
_configTopics.push_back(mqtt_topic_config_led_brightness);
_configTopics.push_back(mqtt_topic_config_auto_unlock);
_configTopics.push_back(mqtt_topic_config_auto_lock);
_configTopics.push_back(mqtt_topic_config_single_lock);
memset(_authName, 0, sizeof(_authName));
_authName[0] = '\0';
_network->registerMqttReceiver(this);
}
NetworkLock::~NetworkLock()
{
}
void NetworkLock::initialize()
{
String mqttPath = _preferences->getString(preference_mqtt_lock_path);
if(mqttPath.length() > 0)
{
size_t len = mqttPath.length();
for(int i=0; i < len; i++)
{
_mqttPath[i] = mqttPath.charAt(i);
}
}
else
{
strcpy(_mqttPath, "nuki");
_preferences->putString(preference_mqtt_lock_path, _mqttPath);
}
_network->setMqttPresencePath(_mqttPath);
_haEnabled = _preferences->getString(preference_mqtt_hass_discovery) != "";
_network->initTopic(_mqttPath, mqtt_topic_lock_action, "--");
_network->subscribe(_mqttPath, mqtt_topic_lock_action);
for(const auto& topic : _configTopics)
{
_network->subscribe(_mqttPath, topic);
}
_network->subscribe(_mqttPath, mqtt_topic_reset);
_network->initTopic(_mqttPath, mqtt_topic_reset, "0");
_network->initTopic(_mqttPath, mqtt_topic_query_config, "0");
_network->initTopic(_mqttPath, mqtt_topic_query_lockstate, "0");
_network->initTopic(_mqttPath, mqtt_topic_query_battery, "0");
_network->subscribe(_mqttPath, mqtt_topic_query_config);
_network->subscribe(_mqttPath, mqtt_topic_query_lockstate);
_network->subscribe(_mqttPath, mqtt_topic_query_battery);
if(_preferences->getBool(preference_keypad_control_enabled))
{
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_action);
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_id);
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_name);
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_code);
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_enabled);
_network->subscribe(_mqttPath, mqtt_topic_query_keypad);
_network->subscribe(_mqttPath, mqtt_topic_keypad_json_action);
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_action, "--");
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_id, "0");
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_name, "--");
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_code, "000000");
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_enabled, "1");
_network->initTopic(_mqttPath, mqtt_topic_query_keypad, "0");
_network->initTopic(_mqttPath, mqtt_topic_keypad_json_action, "--");
}
if(_preferences->getBool(preference_timecontrol_control_enabled))
{
_network->subscribe(_mqttPath, mqtt_topic_timecontrol_action);
_network->initTopic(_mqttPath, mqtt_topic_timecontrol_action, "--");
}
_network->addReconnectedCallback([&]()
{
_reconnected = true;
});
}
void NetworkLock::onMqttDataReceived(const char* topic, byte* payload, const unsigned int length)
{
char* value = (char*)payload;
if(comparePrefixedPath(topic, mqtt_topic_reset) && strcmp(value, "1") == 0)
{
Log->println(F("Restart requested via MQTT."));
_network->clearWifiFallback();
delay(200);
restartEsp(RestartReason::RequestedViaMqtt);
}
if(comparePrefixedPath(topic, mqtt_topic_lock_action))
{
if(strcmp(value, "") == 0 ||
strcmp(value, "--") == 0 ||
strcmp(value, "ack") == 0 ||
strcmp(value, "unknown_action") == 0 ||
strcmp(value, "denied") == 0 ||
strcmp(value, "error") == 0) return;
Log->print(F("Lock action received: "));
Log->println(value);
LockActionResult lockActionResult = LockActionResult::Failed;
if(_lockActionReceivedCallback != NULL)
{
lockActionResult = _lockActionReceivedCallback(value);
}
switch(lockActionResult)
{
case LockActionResult::Success:
publishString(mqtt_topic_lock_action, "ack");
break;
case LockActionResult::UnknownAction:
publishString(mqtt_topic_lock_action, "unknown_action");
break;
case LockActionResult::AccessDenied:
publishString(mqtt_topic_lock_action, "denied");
break;
case LockActionResult::Failed:
publishString(mqtt_topic_lock_action, "error");
break;
}
}
if(comparePrefixedPath(topic, mqtt_topic_keypad_command_action))
{
if(_keypadCommandReceivedReceivedCallback != nullptr)
{
if(strcmp(value, "--") == 0) return;
_keypadCommandReceivedReceivedCallback(value, _keypadCommandId, _keypadCommandName, _keypadCommandCode, _keypadCommandEnabled);
_keypadCommandId = 0;
_keypadCommandName = "--";
_keypadCommandCode = "000000";
_keypadCommandEnabled = 1;
if(strcmp(value, "--") != 0)
{
publishString(mqtt_topic_keypad_command_action, "--");
}
publishInt(mqtt_topic_keypad_command_id, _keypadCommandId);
publishString(mqtt_topic_keypad_command_name, _keypadCommandName);
publishString(mqtt_topic_keypad_command_code, _keypadCommandCode);
publishInt(mqtt_topic_keypad_command_enabled, _keypadCommandEnabled);
}
}
else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_id))
{
_keypadCommandId = atoi(value);
}
else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_name))
{
_keypadCommandName = value;
}
else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_code))
{
_keypadCommandCode = value;
}
else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_enabled))
{
_keypadCommandEnabled = atoi(value);
}
else if(comparePrefixedPath(topic, mqtt_topic_query_config) && strcmp(value, "1") == 0)
{
_queryCommands = _queryCommands | QUERY_COMMAND_CONFIG;
publishString(mqtt_topic_query_config, "0");
}
else if(comparePrefixedPath(topic, mqtt_topic_query_lockstate) && strcmp(value, "1") == 0)
{
_queryCommands = _queryCommands | QUERY_COMMAND_LOCKSTATE;
publishString(mqtt_topic_query_lockstate, "0");
}
else if(comparePrefixedPath(topic, mqtt_topic_query_keypad) && strcmp(value, "1") == 0)
{
_queryCommands = _queryCommands | QUERY_COMMAND_KEYPAD;
publishString(mqtt_topic_query_keypad, "0");
}
else if(comparePrefixedPath(topic, mqtt_topic_query_battery) && strcmp(value, "1") == 0)
{
_queryCommands = _queryCommands | QUERY_COMMAND_BATTERY;
publishString(mqtt_topic_query_battery, "0");
}
for(auto configTopic : _configTopics)
{
if(comparePrefixedPath(topic, configTopic))
{
if(_configUpdateReceivedCallback != nullptr)
{
_configUpdateReceivedCallback(configTopic, value);
}
}
}
if(comparePrefixedPath(topic, mqtt_topic_keypad_json_action))
{
if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return;
if(_keypadJsonCommandReceivedReceivedCallback != NULL)
{
_keypadJsonCommandReceivedReceivedCallback(value);
}
publishString(mqtt_topic_keypad_json_action, "--");
}
if(comparePrefixedPath(topic, mqtt_topic_timecontrol_action))
{
if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return;
if(_timeControlCommandReceivedReceivedCallback != NULL)
{
_timeControlCommandReceivedReceivedCallback(value);
}
publishString(mqtt_topic_timecontrol_action, "--");
}
}
void NetworkLock::publishKeyTurnerState(const NukiLock::KeyTurnerState& keyTurnerState, const NukiLock::KeyTurnerState& lastKeyTurnerState)
{
char str[50];
memset(&str, 0, sizeof(str));
JsonDocument json;
lockstateToString(keyTurnerState.lockState, str);
if((_firstTunerStatePublish || keyTurnerState.lockState != lastKeyTurnerState.lockState) && keyTurnerState.lockState != NukiLock::LockState::Undefined)
{
publishString(mqtt_topic_lock_state, str);
if(_haEnabled)
{
publishState(keyTurnerState.lockState);
}
}
json["lock_state"] = str;
memset(&str, 0, sizeof(str));
triggerToString(keyTurnerState.trigger, str);
if(_firstTunerStatePublish || keyTurnerState.trigger != lastKeyTurnerState.trigger)
{
publishString(mqtt_topic_lock_trigger, str);
}
json["trigger"] = str;
memset(&str, 0, sizeof(str));
lockactionToString(keyTurnerState.lastLockAction, str);
if(_firstTunerStatePublish || keyTurnerState.lastLockAction != lastKeyTurnerState.lastLockAction)
{
publishString(mqtt_topic_lock_last_lock_action, str);
}
json["last_lock_action"] = str;
memset(&str, 0, sizeof(str));
NukiLock::completionStatusToString(keyTurnerState.lastLockActionCompletionStatus, str);
if(_firstTunerStatePublish || keyTurnerState.lastLockActionCompletionStatus != lastKeyTurnerState.lastLockActionCompletionStatus)
{
publishString(mqtt_topic_lock_completionStatus, str);
}
json["lock_completion_status"] = str;
memset(&str, 0, sizeof(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;
if(_firstTunerStatePublish || keyTurnerState.criticalBatteryState != lastKeyTurnerState.criticalBatteryState)
{
bool critical = (keyTurnerState.criticalBatteryState & 0b00000001) > 0;
publishBool(mqtt_topic_battery_critical, critical);
bool charging = (keyTurnerState.criticalBatteryState & 0b00000010) > 0;
publishBool(mqtt_topic_battery_charging, charging);
uint8_t level = (keyTurnerState.criticalBatteryState & 0b11111100) >> 1;
publishInt(mqtt_topic_battery_level, level);
}
if(_firstTunerStatePublish || keyTurnerState.accessoryBatteryState != lastKeyTurnerState.accessoryBatteryState)
{
if ((keyTurnerState.accessoryBatteryState & (1 << 7)) != 0) {
publishBool(mqtt_topic_battery_keypad_critical, (keyTurnerState.accessoryBatteryState & (1 << 6)) != 0);
}
else
{
publishBool(mqtt_topic_battery_keypad_critical, false);
}
}
json["auth_id"] = _authId;
json["auth_name"] = _authName;
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_lock_json, _buffer);
_firstTunerStatePublish = false;
}
void NetworkLock::publishState(NukiLock::LockState lockState)
{
switch(lockState)
{
case NukiLock::LockState::Locked:
publishString(mqtt_topic_lock_ha_state, "locked");
publishString(mqtt_topic_lock_binary_state, "locked");
break;
case NukiLock::LockState::Locking:
publishString(mqtt_topic_lock_ha_state, "locking");
publishString(mqtt_topic_lock_binary_state, "locked");
break;
case NukiLock::LockState::Unlocking:
publishString(mqtt_topic_lock_ha_state, "unlocking");
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::Uncalibrated:
case NukiLock::LockState::Calibration:
case NukiLock::LockState::BootRun:
case NukiLock::LockState::MotorBlocked:
publishString(mqtt_topic_lock_ha_state, "jammed");
break;
default:
break;
}
}
void NetworkLock::publishAuthorizationInfo(const std::list<NukiLock::LogEntry>& logEntries)
{
char str[50];
_authId = 0;
memset(_authName, 0, sizeof(_authName));
_authName[0] = '\0';
_authFound = false;
JsonDocument json;
int i = 5;
for(const auto& log : logEntries)
{
if(i <= 0)
{
break;
}
--i;
if((log.loggingType == NukiLock::LoggingType::LockAction || log.loggingType == NukiLock::LoggingType::KeypadAction) && ! _authFound)
{
_authFound = true;
_authId = log.authId;
int sizeName = sizeof(log.name);
memcpy(_authName, log.name, sizeName);
if(_authName[sizeName - 1] != '\0') _authName[sizeName] = '\0';
}
auto entry = json.add();
entry["index"] = log.index;
entry["authorizationId"] = log.authId;
entry["authorizationName"] = _authName;
entry["timeYear"] = log.timeStampYear;
entry["timeMonth"] = log.timeStampMonth;
entry["timeDay"] = log.timeStampDay;
entry["timeHour"] = log.timeStampHour;
entry["timeMinute"] = log.timeStampMinute;
entry["timeSecond"] = log.timeStampSecond;
memset(str, 0, sizeof(str));
loggingTypeToString(log.loggingType, str);
entry["type"] = str;
switch(log.loggingType)
{
case NukiLock::LoggingType::LockAction:
memset(str, 0, sizeof(str));
NukiLock::lockactionToString((NukiLock::LockAction)log.data[0], str);
entry["action"] = str;
memset(str, 0, sizeof(str));
NukiLock::triggerToString((NukiLock::Trigger)log.data[1], str);
entry["trigger"] = str;
memset(str, 0, sizeof(str));
NukiLock::completionStatusToString((NukiLock::CompletionStatus)log.data[3], str);
entry["completionStatus"] = str;
break;
case NukiLock::LoggingType::KeypadAction:
memset(str, 0, sizeof(str));
NukiLock::lockactionToString((NukiLock::LockAction)log.data[0], str);
entry["action"] = str;
memset(str, 0, sizeof(str));
NukiLock::completionStatusToString((NukiLock::CompletionStatus)log.data[2], str);
entry["completionStatus"] = str;
break;
case NukiLock::LoggingType::DoorSensor:
memset(str, 0, sizeof(str));
NukiLock::lockactionToString((NukiLock::LockAction)log.data[0], str);
switch(log.data[0])
{
case 0:
entry["action"] = "DoorOpened";
break;
case 1:
entry["action"] = "DoorClosed";
break;
case 2:
entry["action"] = "SensorJammed";
break;
default:
entry["action"] = "Unknown";
break;
}
memset(str, 0, sizeof(str));
NukiLock::completionStatusToString((NukiLock::CompletionStatus)log.data[2], str);
entry["completionStatus"] = str;
break;
}
}
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_lock_log, _buffer);
if(_authFound)
{
publishUInt(mqtt_topic_lock_auth_id, _authId);
publishString(mqtt_topic_lock_auth_name, _authName);
}
}
void NetworkLock::clearAuthorizationInfo()
{
publishString(mqtt_topic_lock_log, "--");
publishUInt(mqtt_topic_lock_auth_id, 0);
publishString(mqtt_topic_lock_auth_name, "--");}
void NetworkLock::publishCommandResult(const char *resultStr)
{
publishString(mqtt_topic_lock_action_command_result, resultStr);
}
void NetworkLock::publishLockstateCommandResult(const char *resultStr)
{
publishString(mqtt_topic_query_lockstate_command_result, resultStr);
}
void NetworkLock::publishBatteryReport(const NukiLock::BatteryReport& batteryReport)
{
publishFloat(mqtt_topic_battery_voltage, (float)batteryReport.batteryVoltage / 1000.0);
publishInt(mqtt_topic_battery_drain, batteryReport.batteryDrain); // milliwatt seconds
publishFloat(mqtt_topic_battery_max_turn_current, (float)batteryReport.maxTurnCurrent / 1000.0);
publishInt(mqtt_topic_battery_lock_distance, batteryReport.lockDistance); // degrees
}
void NetworkLock::publishConfig(const NukiLock::Config &config)
{
publishBool(mqtt_topic_config_button_enabled, config.buttonEnabled == 1);
publishBool(mqtt_topic_config_led_enabled, config.ledEnabled == 1);
publishInt(mqtt_topic_config_led_brightness, config.ledBrightness);
publishBool(mqtt_topic_config_single_lock, config.singleLock == 1);
publishString(mqtt_topic_info_firmware_version, std::to_string(config.firmwareVersion[0]) + "." + std::to_string(config.firmwareVersion[1]) + "." + std::to_string(config.firmwareVersion[2]));
publishString(mqtt_topic_info_hardware_version, std::to_string(config.hardwareRevision[0]) + "." + std::to_string(config.hardwareRevision[1]));
}
void NetworkLock::publishAdvancedConfig(const NukiLock::AdvancedConfig &config)
{
publishBool(mqtt_topic_config_auto_unlock, config.autoUnLockDisabled == 0);
publishBool(mqtt_topic_config_auto_lock, config.autoLockEnabled == 1);
}
void NetworkLock::publishRssi(const int& rssi)
{
publishInt(mqtt_topic_lock_rssi, rssi);
}
void NetworkLock::publishRetry(const std::string& message)
{
publishString(mqtt_topic_lock_retry, message);
}
void NetworkLock::publishBleAddress(const std::string &address)
{
publishString(mqtt_topic_lock_address, address);
}
void NetworkLock::publishKeypad(const std::list<NukiLock::KeypadEntry>& entries, uint maxKeypadCodeCount)
{
uint index = 0;
JsonDocument json;
for(const auto& entry : entries)
{
String basePath = mqtt_topic_keypad;
basePath.concat("/code_");
basePath.concat(std::to_string(index).c_str());
publishKeypadEntry(basePath, entry);
auto jsonEntry = json.add();
jsonEntry["codeId"] = entry.codeId;
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;
jsonEntry["lockCount"] = entry.lockCount;
char lastActiveDT[20];
sprintf(lastActiveDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.dateLastActiveYear, entry.dateLastActiveMonth, entry.dateLastActiveDay, entry.dateLastActiveHour, entry.dateLastActiveMin, entry.dateLastActiveSec);
jsonEntry["dateLastActive"] = lastActiveDT;
jsonEntry["timeLimited"] = entry.timeLimited;
char allowedFromDT[20];
sprintf(allowedFromDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.allowedFromYear, entry.allowedFromMonth, entry.allowedFromDay, entry.allowedFromHour, entry.allowedFromMin, entry.allowedFromSec);
jsonEntry["allowedFrom"] = allowedFromDT;
char allowedUntilDT[20];
sprintf(allowedUntilDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.allowedUntilYear, entry.allowedUntilMonth, entry.allowedUntilDay, entry.allowedUntilHour, entry.allowedUntilMin, entry.allowedUntilSec);
jsonEntry["allowedUntil"] = allowedUntilDT;
uint8_t allowedWeekdaysInt = entry.allowedWeekdays;
JsonArray weekdays = jsonEntry["allowedWeekdays"].to<JsonArray>();
while(allowedWeekdaysInt > 0) {
if(allowedWeekdaysInt >= 64)
{
weekdays.add("mon");
allowedWeekdaysInt -= 64;
continue;
}
if(allowedWeekdaysInt >= 32)
{
weekdays.add("tue");
allowedWeekdaysInt -= 32;
continue;
}
if(allowedWeekdaysInt >= 16)
{
weekdays.add("wed");
allowedWeekdaysInt -= 16;
continue;
}
if(allowedWeekdaysInt >= 8)
{
weekdays.add("thu");
allowedWeekdaysInt -= 8;
continue;
}
if(allowedWeekdaysInt >= 4)
{
weekdays.add("fri");
allowedWeekdaysInt -= 4;
continue;
}
if(allowedWeekdaysInt >= 2)
{
weekdays.add("sat");
allowedWeekdaysInt -= 2;
continue;
}
if(allowedWeekdaysInt >= 1)
{
weekdays.add("sun");
allowedWeekdaysInt -= 1;
continue;
}
}
char allowedFromTimeT[5];
sprintf(allowedFromTimeT, "%02d:%02d", entry.allowedFromTimeHour, entry.allowedFromTimeMin);
jsonEntry["allowedFromTime"] = allowedFromTimeT;
char allowedUntilTimeT[5];
sprintf(allowedUntilTimeT, "%02d:%02d", entry.allowedUntilTimeHour, entry.allowedUntilTimeMin);
jsonEntry["allowedUntilTime"] = allowedUntilTimeT;
++index;
}
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_keypad_json, _buffer);
while(index < maxKeypadCodeCount)
{
NukiLock::KeypadEntry entry;
memset(&entry, 0, sizeof(entry));
String basePath = mqtt_topic_keypad;
basePath.concat("/code_");
basePath.concat(std::to_string(index).c_str());
publishKeypadEntry(basePath, entry);
++index;
}
}
void NetworkLock::publishTimeControl(const std::list<NukiLock::TimeControlEntry>& timeControlEntries)
{
char str[50];
JsonDocument json;
for(const auto& entry : timeControlEntries)
{
auto jsonEntry = json.add();
jsonEntry["entryId"] = entry.entryId;
jsonEntry["enabled"] = entry.enabled;
uint8_t weekdaysInt = entry.weekdays;
JsonArray weekdays = jsonEntry["weekdays"].to<JsonArray>();
while(weekdaysInt > 0) {
if(weekdaysInt >= 64)
{
weekdays.add("mon");
weekdaysInt -= 64;
continue;
}
if(weekdaysInt >= 32)
{
weekdays.add("tue");
weekdaysInt -= 32;
continue;
}
if(weekdaysInt >= 16)
{
weekdays.add("wed");
weekdaysInt -= 16;
continue;
}
if(weekdaysInt >= 8)
{
weekdays.add("thu");
weekdaysInt -= 8;
continue;
}
if(weekdaysInt >= 4)
{
weekdays.add("fri");
weekdaysInt -= 4;
continue;
}
if(weekdaysInt >= 2)
{
weekdays.add("sat");
weekdaysInt -= 2;
continue;
}
if(weekdaysInt >= 1)
{
weekdays.add("sun");
weekdaysInt -= 1;
continue;
}
}
char timeT[5];
sprintf(timeT, "%02d:%02d", entry.timeHour, entry.timeMin);
jsonEntry["time"] = timeT;
memset(str, 0, sizeof(str));
NukiLock::lockactionToString(entry.lockAction, str);
jsonEntry["lockAction"] = str;
}
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_timecontrol_json, _buffer);
}
void NetworkLock::publishKeypadCommandResult(const char* result)
{
publishString(mqtt_topic_keypad_command_result, result);
}
void NetworkLock::publishKeypadJsonCommandResult(const char* result)
{
publishString(mqtt_topic_keypad_json_command_result, result);
}
void NetworkLock::publishTimeControlCommandResult(const char* result)
{
publishString(mqtt_topic_timecontrol_command_result, result);
}
void NetworkLock::setLockActionReceivedCallback(LockActionResult (*lockActionReceivedCallback)(const char *))
{
_lockActionReceivedCallback = lockActionReceivedCallback;
}
void NetworkLock::setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char *, const char *))
{
_configUpdateReceivedCallback = configUpdateReceivedCallback;
}
void NetworkLock::setKeypadCommandReceivedCallback(void (*keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled))
{
_keypadCommandReceivedReceivedCallback = keypadCommandReceivedReceivedCallback;
}
void NetworkLock::setKeypadJsonCommandReceivedCallback(void (*keypadJsonCommandReceivedReceivedCallback)(const char *))
{
_keypadJsonCommandReceivedReceivedCallback = keypadJsonCommandReceivedReceivedCallback;
}
void NetworkLock::setTimeControlCommandReceivedCallback(void (*timeControlCommandReceivedReceivedCallback)(const char *))
{
_timeControlCommandReceivedReceivedCallback = timeControlCommandReceivedReceivedCallback;
}
void NetworkLock::buildMqttPath(const char* path, char* outPath)
{
int offset = 0;
for(const char& c : _mqttPath)
{
if(c == 0x00)
{
break;
}
outPath[offset] = c;
++offset;
}
int i=0;
while(outPath[i] != 0x00)
{
outPath[offset] = path[i];
++i;
++offset;
}
outPath[i+1] = 0x00;
}
bool NetworkLock::comparePrefixedPath(const char *fullPath, const char *subPath)
{
char prefixedPath[500];
buildMqttPath(subPath, prefixedPath);
return strcmp(fullPath, prefixedPath) == 0;
}
void NetworkLock::publishHASSConfig(char *deviceType, const char *baseTopic, char *name, char *uidString, const bool& hasDoorSensor, const bool& hasKeypad, const bool& publishAuthData, char *lockAction,
char *unlockAction, char *openAction)
{
_network->publishHASSConfig(deviceType, baseTopic, name, uidString, "~/maintenance/mqttConnectionState", hasKeypad, lockAction, unlockAction, openAction);
_network->publishHASSConfigAdditionalButtons(deviceType, baseTopic, name, uidString);
_network->publishHASSConfigBatLevel(deviceType, baseTopic, name, uidString);
_network->publishHASSConfigLedBrightness(deviceType, baseTopic, name, uidString);
if(hasDoorSensor)
{
_network->publishHASSConfigDoorSensor(deviceType, baseTopic, name, uidString);
}
else
{
_network->removeHASSConfigTopic("binary_sensor", "door_sensor", uidString);
}
_network->publishHASSWifiRssiConfig(deviceType, baseTopic, name, uidString);
_network->publishHASSBleRssiConfig(deviceType, baseTopic, name, uidString);
if(publishAuthData)
{
_network->publishHASSConfigAccessLog(deviceType, baseTopic, name, uidString);
}
else
{
_network->removeHASSConfigTopic("sensor", "last_action_authorization", uidString);
}
if(hasKeypad)
{
_network->publishHASSConfigKeypadAttemptInfo(deviceType, baseTopic, name, uidString);
}
else
{
_network->removeHASSConfigTopic("sensor", "keypad_status", uidString);
}
}
void NetworkLock::removeHASSConfig(char *uidString)
{
_network->removeHASSConfig(uidString);
}
void NetworkLock::publishFloat(const char *topic, const float value, const uint8_t precision)
{
_network->publishFloat(_mqttPath, topic, value, precision);
}
void NetworkLock::publishInt(const char *topic, const int value)
{
_network->publishInt(_mqttPath, topic, value);
}
void NetworkLock::publishUInt(const char *topic, const unsigned int value)
{
_network->publishUInt(_mqttPath, topic, value);
}
void NetworkLock::publishBool(const char *topic, const bool value)
{
_network->publishBool(_mqttPath, topic, value);
}
bool NetworkLock::publishString(const char *topic, const String &value)
{
char str[value.length() + 1];
memset(str, 0, sizeof(str));
memcpy(str, value.begin(), value.length());
return publishString(topic, str);
}
bool NetworkLock::publishString(const char *topic, const std::string &value)
{
char str[value.size() + 1];
memset(str, 0, sizeof(str));
memcpy(str, value.data(), value.length());
return publishString(topic, str);
}
bool NetworkLock::publishString(const char *topic, const char *value)
{
return _network->publishString(_mqttPath, topic, value);
}
void NetworkLock::publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry)
{
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);
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);
}
String NetworkLock::concat(String a, String b)
{
String c = a;
c.concat(b);
return c;
}
bool NetworkLock::reconnected()
{
bool r = _reconnected;
_reconnected = false;
return r;
}
uint8_t NetworkLock::queryCommands()
{
uint8_t qc = _queryCommands;
_queryCommands = 0;
return qc;
}

100
src/NetworkLock.h Normal file
View File

@@ -0,0 +1,100 @@
#pragma once
#include "networkDevices/NetworkDevice.h"
#include "networkDevices/WifiDevice.h"
#include "networkDevices/W5500Device.h"
#include <Preferences.h>
#include <vector>
#include <list>
#include "NukiConstants.h"
#include "NukiLockConstants.h"
#include "Network.h"
#include "QueryCommand.h"
#include "LockActionResult.h"
#define LOCK_LOG_JSON_BUFFER_SIZE 2048
class NetworkLock : public MqttReceiver
{
public:
explicit NetworkLock(Network* network, Preferences* preferences, char* buffer, size_t bufferSize);
virtual ~NetworkLock();
void initialize();
void publishKeyTurnerState(const NukiLock::KeyTurnerState& keyTurnerState, const NukiLock::KeyTurnerState& lastKeyTurnerState);
void publishState(NukiLock::LockState lockState);
void publishAuthorizationInfo(const std::list<NukiLock::LogEntry>& logEntries);
void clearAuthorizationInfo();
void publishCommandResult(const char* resultStr);
void publishLockstateCommandResult(const char* resultStr);
void publishBatteryReport(const NukiLock::BatteryReport& batteryReport);
void publishConfig(const NukiLock::Config& config);
void publishAdvancedConfig(const NukiLock::AdvancedConfig& config);
void publishRssi(const int& rssi);
void publishRetry(const std::string& message);
void publishBleAddress(const std::string& address);
void publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, 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 publishKeypadCommandResult(const char* result);
void publishKeypadJsonCommandResult(const char* result);
void publishTimeControlCommandResult(const char* result);
void setLockActionReceivedCallback(LockActionResult (*lockActionReceivedCallback)(const char* value));
void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, 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;
bool reconnected();
uint8_t queryCommands();
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 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);
String concat(String a, String b);
void buildMqttPath(const char* path, char* outPath);
Network* _network;
Preferences* _preferences;
std::vector<char*> _configTopics;
char _mqttPath[181] = {0};
bool _firstTunerStatePublish = true;
unsigned long _lastMaintenanceTs = 0;
bool _haEnabled = false;
bool _reconnected = false;
String _keypadCommandName = "";
String _keypadCommandCode = "";
uint _keypadCommandId = 0;
int _keypadCommandEnabled = 1;
uint8_t _queryCommands = 0;
uint32_t _authId = 0;
char _authName[33];
bool _authFound = false;
char* _buffer;
size_t _bufferSize;
LockActionResult (*_lockActionReceivedCallback)(const char* value) = nullptr;
void (*_configUpdateReceivedCallback)(const char* path, 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;
void (*_timeControlCommandReceivedReceivedCallback)(const char* value) = nullptr;
};

922
src/NetworkOpener.cpp Normal file
View File

@@ -0,0 +1,922 @@
#include "NetworkOpener.h"
#include "Arduino.h"
#include "MqttTopics.h"
#include "PreferencesKeys.h"
#include "Logger.h"
#include "Config.h"
#include <ArduinoJson.h>
NetworkOpener::NetworkOpener(Network* network, Preferences* preferences, char* buffer, size_t bufferSize)
: _preferences(preferences),
_network(network),
_buffer(buffer),
_bufferSize(bufferSize)
{
_configTopics.reserve(5);
_configTopics.push_back(mqtt_topic_config_button_enabled);
_configTopics.push_back(mqtt_topic_config_led_enabled);
_configTopics.push_back(mqtt_topic_config_sound_level);
memset(_authName, 0, sizeof(_authName));
_authName[0] = '\0';
_network->registerMqttReceiver(this);
}
void NetworkOpener::initialize()
{
String mqttPath = _preferences->getString(preference_mqtt_opener_path);
if(mqttPath.length() > 0)
{
size_t len = mqttPath.length();
for(int i=0; i < len; i++)
{
_mqttPath[i] = mqttPath.charAt(i);
}
}
else
{
strcpy(_mqttPath, "nukiopener");
_preferences->putString(preference_mqtt_opener_path, _mqttPath);
}
_haEnabled = _preferences->getString(preference_mqtt_hass_discovery) != "";
_network->initTopic(_mqttPath, mqtt_topic_lock_action, "--");
_network->subscribe(_mqttPath, mqtt_topic_lock_action);
for(const auto& topic : _configTopics)
{
_network->subscribe(_mqttPath, topic);
}
_network->initTopic(_mqttPath, mqtt_topic_query_config, "0");
_network->initTopic(_mqttPath, mqtt_topic_query_lockstate, "0");
_network->initTopic(_mqttPath, mqtt_topic_query_battery, "0");
_network->initTopic(_mqttPath, mqtt_topic_lock_binary_ring, "standby");
_network->subscribe(_mqttPath, mqtt_topic_query_config);
_network->subscribe(_mqttPath, mqtt_topic_query_lockstate);
_network->subscribe(_mqttPath, mqtt_topic_query_battery);
if(_preferences->getBool(preference_keypad_control_enabled))
{
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_action);
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_id);
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_name);
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_code);
_network->subscribe(_mqttPath, mqtt_topic_keypad_command_enabled);
_network->subscribe(_mqttPath, mqtt_topic_query_keypad);
_network->subscribe(_mqttPath, mqtt_topic_keypad_json_action);
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_action, "--");
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_id, "0");
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_name, "--");
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_code, "000000");
_network->initTopic(_mqttPath, mqtt_topic_keypad_command_enabled, "1");
_network->initTopic(_mqttPath, mqtt_topic_query_keypad, "0");
_network->initTopic(_mqttPath, mqtt_topic_keypad_json_action, "--");
}
if(_preferences->getBool(preference_timecontrol_control_enabled))
{
_network->subscribe(_mqttPath, mqtt_topic_timecontrol_action);
_network->initTopic(_mqttPath, mqtt_topic_timecontrol_action, "--");
}
_network->addReconnectedCallback([&]()
{
_reconnected = true;
});
}
void NetworkOpener::update()
{
if(_resetRingStateTs != 0 && millis() >= _resetRingStateTs)
{
_resetRingStateTs = 0;
publishString(mqtt_topic_lock_binary_ring, "standby");
}
}
void NetworkOpener::onMqttDataReceived(const char* topic, byte* payload, const unsigned int length)
{
char* value = (char*)payload;
if(comparePrefixedPath(topic, mqtt_topic_lock_action))
{
if(strcmp(value, "") == 0 ||
strcmp(value, "--") == 0 ||
strcmp(value, "ack") == 0 ||
strcmp(value, "unknown_action") == 0 ||
strcmp(value, "denied") == 0 ||
strcmp(value, "error") == 0) return;
Log->print(F("Lock action received: "));
Log->println(value);
LockActionResult lockActionResult = LockActionResult::Failed;
if(_lockActionReceivedCallback != NULL)
{
lockActionResult = _lockActionReceivedCallback(value);
}
switch(lockActionResult)
{
case LockActionResult::Success:
publishString(mqtt_topic_lock_action, "ack");
break;
case LockActionResult::UnknownAction:
publishString(mqtt_topic_lock_action, "unknown_action");
break;
case LockActionResult::AccessDenied:
publishString(mqtt_topic_lock_action, "denied");
break;
case LockActionResult::Failed:
publishString(mqtt_topic_lock_action, "error");
break;
}
}
if(comparePrefixedPath(topic, mqtt_topic_keypad_command_action))
{
if(_keypadCommandReceivedReceivedCallback != nullptr)
{
if(strcmp(value, "--") == 0) return;
_keypadCommandReceivedReceivedCallback(value, _keypadCommandId, _keypadCommandName, _keypadCommandCode, _keypadCommandEnabled);
_keypadCommandId = 0;
_keypadCommandName = "--";
_keypadCommandCode = "000000";
_keypadCommandEnabled = 1;
if(strcmp(value, "--") != 0)
{
publishString(mqtt_topic_keypad_command_action, "--");
}
publishInt(mqtt_topic_keypad_command_id, _keypadCommandId);
publishString(mqtt_topic_keypad_command_name, _keypadCommandName);
publishString(mqtt_topic_keypad_command_code, _keypadCommandCode);
publishInt(mqtt_topic_keypad_command_enabled, _keypadCommandEnabled);
}
}
else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_id))
{
_keypadCommandId = atoi(value);
}
else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_name))
{
_keypadCommandName = value;
}
else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_code))
{
_keypadCommandCode = value;
}
else if(comparePrefixedPath(topic, mqtt_topic_keypad_command_enabled))
{
_keypadCommandEnabled = atoi(value);
}
else if(comparePrefixedPath(topic, mqtt_topic_query_config) && strcmp(value, "1") == 0)
{
_queryCommands = _queryCommands | QUERY_COMMAND_CONFIG;
publishString(mqtt_topic_query_config, "0");
}
else if(comparePrefixedPath(topic, mqtt_topic_query_lockstate) && strcmp(value, "1") == 0)
{
_queryCommands = _queryCommands | QUERY_COMMAND_LOCKSTATE;
publishString(mqtt_topic_query_lockstate, "0");
}
else if(comparePrefixedPath(topic, mqtt_topic_query_keypad) && strcmp(value, "1") == 0)
{
_queryCommands = _queryCommands | QUERY_COMMAND_KEYPAD;
publishString(mqtt_topic_query_keypad, "0");
}
else if(comparePrefixedPath(topic, mqtt_topic_query_battery) && strcmp(value, "1") == 0)
{
_queryCommands = _queryCommands | QUERY_COMMAND_BATTERY;
publishString(mqtt_topic_query_battery, "0");
}
for(auto configTopic : _configTopics)
{
if(comparePrefixedPath(topic, configTopic))
{
if(_configUpdateReceivedCallback != nullptr)
{
_configUpdateReceivedCallback(configTopic, value);
}
}
}
if(comparePrefixedPath(topic, mqtt_topic_keypad_json_action))
{
if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return;
if(_keypadJsonCommandReceivedReceivedCallback != NULL)
{
_keypadJsonCommandReceivedReceivedCallback(value);
}
publishString(mqtt_topic_keypad_json_action, "--");
}
if(comparePrefixedPath(topic, mqtt_topic_timecontrol_action))
{
if(strcmp(value, "") == 0 || strcmp(value, "--") == 0) return;
if(_timeControlCommandReceivedReceivedCallback != NULL)
{
_timeControlCommandReceivedReceivedCallback(value);
}
publishString(mqtt_topic_timecontrol_action, "--");
}
}
void NetworkOpener::publishKeyTurnerState(const NukiOpener::OpenerState& keyTurnerState, const NukiOpener::OpenerState& lastKeyTurnerState)
{
_currentLockState = keyTurnerState.lockState;
char str[50];
memset(&str, 0, sizeof(str));
JsonDocument json;
lockstateToString(keyTurnerState.lockState, str);
if((_firstTunerStatePublish || keyTurnerState.lockState != lastKeyTurnerState.lockState || keyTurnerState.nukiState != lastKeyTurnerState.nukiState) && keyTurnerState.lockState != NukiOpener::LockState::Undefined)
{
publishString(mqtt_topic_lock_state, str);
if(_haEnabled)
{
publishState(keyTurnerState);
}
}
json["lock_state"] = str;
if(keyTurnerState.nukiState == NukiOpener::State::ContinuousMode)
{
publishString(mqtt_topic_lock_continuous_mode, "on");
json["continuous_mode"] = 1;
} else {
publishString(mqtt_topic_lock_continuous_mode, "off");
json["continuous_mode"] = 0;
}
memset(&str, 0, sizeof(str));
triggerToString(keyTurnerState.trigger, str);
if(_firstTunerStatePublish || keyTurnerState.trigger != lastKeyTurnerState.trigger)
{
publishString(mqtt_topic_lock_trigger, str);
}
json["trigger"] = str;
memset(&str, 0, sizeof(str));
completionStatusToString(keyTurnerState.lastLockActionCompletionStatus, str);
if(_firstTunerStatePublish || keyTurnerState.lastLockActionCompletionStatus != lastKeyTurnerState.lastLockActionCompletionStatus)
{
publishString(mqtt_topic_lock_completionStatus, str);
}
json["lock_completion_status"] = str;
memset(&str, 0, sizeof(str));
NukiOpener::doorSensorStateToString(keyTurnerState.doorSensorState, str);
if(_firstTunerStatePublish || keyTurnerState.doorSensorState != lastKeyTurnerState.doorSensorState)
{
publishString(mqtt_topic_lock_door_sensor_state, str);
}
json["door_sensor_state"] = str;
if(_firstTunerStatePublish || keyTurnerState.criticalBatteryState != lastKeyTurnerState.criticalBatteryState)
{
bool critical = (keyTurnerState.criticalBatteryState & 0b00000001) > 0;
publishBool(mqtt_topic_battery_critical, critical);
}
json["auth_id"] = _authId;
json["auth_name"] = _authName;
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_lock_json, _buffer);
_firstTunerStatePublish = false;
}
void NetworkOpener::publishRing(const bool locked)
{
if (locked)
{
publishString(mqtt_topic_lock_ring, "ringlocked");
}
else
{
publishString(mqtt_topic_lock_ring, "ring");
}
publishString(mqtt_topic_lock_binary_ring, "ring");
_resetRingStateTs = millis() + 2000;
}
void NetworkOpener::publishState(NukiOpener::OpenerState lockState)
{
if(lockState.nukiState == NukiOpener::State::ContinuousMode)
{
publishString(mqtt_topic_lock_ha_state, "unlocked");
publishString(mqtt_topic_lock_binary_state, "unlocked");
}
else
{
switch (lockState.lockState)
{
case NukiOpener::LockState::Locked:
publishString(mqtt_topic_lock_ha_state, "locked");
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::Opening:
publishString(mqtt_topic_lock_ha_state, "unlocking");
publishString(mqtt_topic_lock_binary_state, "unlocked");
break;
case NukiOpener::LockState::Undefined:
case NukiOpener::LockState::Uncalibrated:
publishString(mqtt_topic_lock_ha_state, "jammed");
break;
default:
break;
}
}
}
void NetworkOpener::publishAuthorizationInfo(const std::list<NukiOpener::LogEntry>& logEntries)
{
char str[50];
_authId = 0;
memset(_authName, 0, sizeof(_authName));
_authName[0] = '\0';
_authFound = false;
JsonDocument json;
int i = 5;
for(const auto& log : logEntries)
{
if(i <= 0)
{
break;
}
--i;
if((log.loggingType == NukiOpener::LoggingType::LockAction || log.loggingType == NukiOpener::LoggingType::KeypadAction) && ! _authFound)
{
_authFound = true;
_authId = log.authId;
int sizeName = sizeof(log.name);
memcpy(_authName, log.name, sizeName);
if(_authName[sizeName - 1] != '\0') _authName[sizeName] = '\0';
}
auto entry = json.add();
entry["index"] = log.index;
entry["authorizationId"] = log.authId;
entry["authorizationName"] = _authName;
entry["timeYear"] = log.timeStampYear;
entry["timeMonth"] = log.timeStampMonth;
entry["timeDay"] = log.timeStampDay;
entry["timeHour"] = log.timeStampHour;
entry["timeMinute"] = log.timeStampMinute;
entry["timeSecond"] = log.timeStampSecond;
memset(str, 0, sizeof(str));
loggingTypeToString(log.loggingType, str);
entry["type"] = str;
switch(log.loggingType)
{
case NukiOpener::LoggingType::LockAction:
memset(str, 0, sizeof(str));
NukiOpener::lockactionToString((NukiOpener::LockAction)log.data[0], str);
entry["action"] = str;
memset(str, 0, sizeof(str));
NukiOpener::triggerToString((NukiOpener::Trigger)log.data[1], str);
entry["trigger"] = str;
memset(str, 0, sizeof(str));
NukiOpener::completionStatusToString((NukiOpener::CompletionStatus)log.data[3], str);
entry["completionStatus"] = str;
break;
case NukiOpener::LoggingType::KeypadAction:
memset(str, 0, sizeof(str));
NukiOpener::lockactionToString((NukiOpener::LockAction)log.data[0], str);
entry["action"] = str;
memset(str, 0, sizeof(str));
NukiOpener::completionStatusToString((NukiOpener::CompletionStatus)log.data[2], str);
entry["completionStatus"] = str;
break;
case NukiOpener::LoggingType::DoorbellRecognition:
switch(log.data[0] & 3)
{
case 0:
entry["mode"] = "None";
break;
case 1:
entry["mode"] = "RTO";
break;
case 2:
entry["mode"] = "CM";
break;
default:
entry["mode"] = "Unknown";
break;
}
switch(log.data[1])
{
case 0:
entry["source"] = "Doorbell";
break;
case 1:
entry["source"] = "Timecontrol";
break;
case 2:
entry["source"] = "App";
break;
case 3:
entry["source"] = "Button";
break;
case 4:
entry["source"] = "Fob";
break;
case 5:
entry["source"] = "Bridge";
break;
case 6:
entry["source"] = "Keypad";
break;
default:
entry["source"] = "Unknown";
break; }
entry["geofence"] = log.data[2] == 1 ? "active" : "inactive";
entry["doorbellSuppression"] = log.data[3] == 1 ? "active" : "inactive";
entry["completionStatus"] = str;
break;
}
}
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_lock_log, _buffer);
if(_authFound)
{
publishUInt(mqtt_topic_lock_auth_id, _authId);
publishString(mqtt_topic_lock_auth_name, _authName);
}
}
void NetworkOpener::logactionCompletionStatusToString(uint8_t value, char* out)
{
switch (value)
{
case 0x00:
strcpy(out, "success");
break;
case 0x02:
strcpy(out, "cancelled");
break;
case 0x03:
strcpy(out, "tooRecent");
break;
case 0x04:
strcpy(out, "busy");
break;
case 0x08:
strcpy(out, "incomplete");
break;
case 0xfe:
strcpy(out, "otherError");
break;
case 0xff:
strcpy(out, "unknown");
break;
default:
strcpy(out, "undefined");
break;
}
}
void NetworkOpener::clearAuthorizationInfo()
{
publishString(mqtt_topic_lock_log, "--");
publishUInt(mqtt_topic_lock_auth_id, 0);
publishString(mqtt_topic_lock_auth_name, "--");
}
void NetworkOpener::publishCommandResult(const char *resultStr)
{
publishString(mqtt_topic_lock_action_command_result, resultStr);
}
void NetworkOpener::publishLockstateCommandResult(const char *resultStr)
{
publishString(mqtt_topic_query_lockstate_command_result, resultStr);
}
void NetworkOpener::publishBatteryReport(const NukiOpener::BatteryReport& batteryReport)
{
publishFloat(mqtt_topic_battery_voltage, (float)batteryReport.batteryVoltage / 1000.0);
}
void NetworkOpener::publishConfig(const NukiOpener::Config &config)
{
publishBool(mqtt_topic_config_button_enabled, config.buttonEnabled == 1);
publishBool(mqtt_topic_config_led_enabled, config.ledFlashEnabled == 1);
publishString(mqtt_topic_info_firmware_version, std::to_string(config.firmwareVersion[0]) + "." + std::to_string(config.firmwareVersion[1]) + "." + std::to_string(config.firmwareVersion[2]));
publishString(mqtt_topic_info_hardware_version, std::to_string(config.hardwareRevision[0]) + "." + std::to_string(config.hardwareRevision[1]));
}
void NetworkOpener::publishAdvancedConfig(const NukiOpener::AdvancedConfig &config)
{
publishUInt(mqtt_topic_config_sound_level, config.soundLevel);
}
void NetworkOpener::publishRssi(const int &rssi)
{
publishInt(mqtt_topic_lock_rssi, rssi);
}
void NetworkOpener::publishRetry(const std::string& message)
{
publishString(mqtt_topic_lock_retry, message);
}
void NetworkOpener::publishBleAddress(const std::string &address)
{
publishString(mqtt_topic_lock_address, address);
}
void NetworkOpener::publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, char* lockAction, char* unlockAction, char* openAction)
{
String availabilityTopic = _preferences->getString("mqttpath");
availabilityTopic.concat("/maintenance/mqttConnectionState");
_network->publishHASSConfig(deviceType, baseTopic, name, uidString, availabilityTopic.c_str(), false, lockAction, unlockAction, openAction);
_network->publishHASSConfigRingDetect(deviceType, baseTopic, name, uidString);
_network->publishHASSConfigContinuousMode(deviceType, baseTopic, name, uidString);
_network->publishHASSConfigSoundLevel(deviceType, baseTopic, name, uidString);
_network->publishHASSBleRssiConfig(deviceType, baseTopic, name, uidString);
}
void NetworkOpener::removeHASSConfig(char* uidString)
{
_network->removeHASSConfig(uidString);
}
void NetworkOpener::publishKeypad(const std::list<NukiLock::KeypadEntry>& entries, uint maxKeypadCodeCount)
{
uint index = 0;
JsonDocument json;
for(const auto& entry : entries)
{
String basePath = mqtt_topic_keypad;
basePath.concat("/code_");
basePath.concat(std::to_string(index).c_str());
publishKeypadEntry(basePath, entry);
auto jsonEntry = json.add();
jsonEntry["codeId"] = entry.codeId;
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;
jsonEntry["lockCount"] = entry.lockCount;
char lastActiveDT[20];
sprintf(lastActiveDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.dateLastActiveYear, entry.dateLastActiveMonth, entry.dateLastActiveDay, entry.dateLastActiveHour, entry.dateLastActiveMin, entry.dateLastActiveSec);
jsonEntry["dateLastActive"] = lastActiveDT;
jsonEntry["timeLimited"] = entry.timeLimited;
char allowedFromDT[20];
sprintf(allowedFromDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.allowedFromYear, entry.allowedFromMonth, entry.allowedFromDay, entry.allowedFromHour, entry.allowedFromMin, entry.allowedFromSec);
jsonEntry["allowedFrom"] = allowedFromDT;
char allowedUntilDT[20];
sprintf(allowedUntilDT, "%04d-%02d-%02d %02d:%02d:%02d", entry.allowedUntilYear, entry.allowedUntilMonth, entry.allowedUntilDay, entry.allowedUntilHour, entry.allowedUntilMin, entry.allowedUntilSec);
jsonEntry["allowedUntil"] = allowedUntilDT;
uint8_t allowedWeekdaysInt = entry.allowedWeekdays;
JsonArray weekdays = jsonEntry["allowedWeekdays"].to<JsonArray>();
while(allowedWeekdaysInt > 0) {
if(allowedWeekdaysInt >= 64)
{
weekdays.add("mon");
allowedWeekdaysInt -= 64;
continue;
}
if(allowedWeekdaysInt >= 32)
{
weekdays.add("tue");
allowedWeekdaysInt -= 32;
continue;
}
if(allowedWeekdaysInt >= 16)
{
weekdays.add("wed");
allowedWeekdaysInt -= 16;
continue;
}
if(allowedWeekdaysInt >= 8)
{
weekdays.add("thu");
allowedWeekdaysInt -= 8;
continue;
}
if(allowedWeekdaysInt >= 4)
{
weekdays.add("fri");
allowedWeekdaysInt -= 4;
continue;
}
if(allowedWeekdaysInt >= 2)
{
weekdays.add("sat");
allowedWeekdaysInt -= 2;
continue;
}
if(allowedWeekdaysInt >= 1)
{
weekdays.add("sun");
allowedWeekdaysInt -= 1;
continue;
}
}
char allowedFromTimeT[5];
sprintf(allowedFromTimeT, "%02d:%02d", entry.allowedFromTimeHour, entry.allowedFromTimeMin);
jsonEntry["allowedFromTime"] = allowedFromTimeT;
char allowedUntilTimeT[5];
sprintf(allowedUntilTimeT, "%02d:%02d", entry.allowedUntilTimeHour, entry.allowedUntilTimeMin);
jsonEntry["allowedUntilTime"] = allowedUntilTimeT;
++index;
}
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_keypad_json, _buffer);
while(index < maxKeypadCodeCount)
{
NukiLock::KeypadEntry entry;
memset(&entry, 0, sizeof(entry));
String basePath = mqtt_topic_keypad;
basePath.concat("/code_");
basePath.concat(std::to_string(index).c_str());
publishKeypadEntry(basePath, entry);
++index;
}
}
void NetworkOpener::publishTimeControl(const std::list<NukiOpener::TimeControlEntry>& timeControlEntries)
{
char str[50];
JsonDocument json;
for(const auto& entry : timeControlEntries)
{
auto jsonEntry = json.add();
jsonEntry["entryId"] = entry.entryId;
jsonEntry["enabled"] = entry.enabled;
uint8_t weekdaysInt = entry.weekdays;
JsonArray weekdays = jsonEntry["weekdays"].to<JsonArray>();
while(weekdaysInt > 0) {
if(weekdaysInt >= 64)
{
weekdays.add("mon");
weekdaysInt -= 64;
continue;
}
if(weekdaysInt >= 32)
{
weekdays.add("tue");
weekdaysInt -= 32;
continue;
}
if(weekdaysInt >= 16)
{
weekdays.add("wed");
weekdaysInt -= 16;
continue;
}
if(weekdaysInt >= 8)
{
weekdays.add("thu");
weekdaysInt -= 8;
continue;
}
if(weekdaysInt >= 4)
{
weekdays.add("fri");
weekdaysInt -= 4;
continue;
}
if(weekdaysInt >= 2)
{
weekdays.add("sat");
weekdaysInt -= 2;
continue;
}
if(weekdaysInt >= 1)
{
weekdays.add("sun");
weekdaysInt -= 1;
continue;
}
}
char timeT[5];
sprintf(timeT, "%02d:%02d", entry.timeHour, entry.timeMin);
jsonEntry["time"] = timeT;
memset(str, 0, sizeof(str));
NukiOpener::lockactionToString(entry.lockAction, str);
jsonEntry["lockAction"] = str;
}
serializeJson(json, _buffer, _bufferSize);
publishString(mqtt_topic_timecontrol_json, _buffer);
}
void NetworkOpener::publishKeypadCommandResult(const char* result)
{
publishString(mqtt_topic_keypad_command_result, result);
}
void NetworkOpener::publishKeypadJsonCommandResult(const char* result)
{
publishString(mqtt_topic_keypad_json_command_result, result);
}
void NetworkOpener::publishTimeControlCommandResult(const char* result)
{
publishString(mqtt_topic_timecontrol_command_result, result);
}
void NetworkOpener::setLockActionReceivedCallback(LockActionResult (*lockActionReceivedCallback)(const char *))
{
_lockActionReceivedCallback = lockActionReceivedCallback;
}
void NetworkOpener::setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char *, const char *))
{
_configUpdateReceivedCallback = configUpdateReceivedCallback;
}
void NetworkOpener::setKeypadCommandReceivedCallback(void (*keypadCommandReceivedReceivedCallback)(const char* command, const uint& id, const String& name, const String& code, const int& enabled))
{
_keypadCommandReceivedReceivedCallback = keypadCommandReceivedReceivedCallback;
}
void NetworkOpener::setKeypadJsonCommandReceivedCallback(void (*keypadJsonCommandReceivedReceivedCallback)(const char *))
{
_keypadJsonCommandReceivedReceivedCallback = keypadJsonCommandReceivedReceivedCallback;
}
void NetworkOpener::setTimeControlCommandReceivedCallback(void (*timeControlCommandReceivedReceivedCallback)(const char *))
{
_timeControlCommandReceivedReceivedCallback = timeControlCommandReceivedReceivedCallback;
}
void NetworkOpener::publishFloat(const char *topic, const float value, const uint8_t precision)
{
_network->publishFloat(_mqttPath, topic, value, precision);
}
void NetworkOpener::publishInt(const char *topic, const int value)
{
_network->publishInt(_mqttPath, topic, value);
}
void NetworkOpener::publishUInt(const char *topic, const unsigned int value)
{
_network->publishUInt(_mqttPath, topic, value);
}
void NetworkOpener::publishBool(const char *topic, const bool value)
{
_network->publishBool(_mqttPath, topic, value);
}
void NetworkOpener::publishString(const char *topic, const String &value)
{
char str[value.length() + 1];
memset(str, 0, sizeof(str));
memcpy(str, value.begin(), value.length());
publishString(topic, str);
}
void NetworkOpener::publishString(const char *topic, const std::string &value)
{
char str[value.size() + 1];
memset(str, 0, sizeof(str));
memcpy(str, value.data(), value.length());
publishString(topic, str);
}
void NetworkOpener::publishString(const char* topic, const char* value)
{
_network->publishString(_mqttPath, topic, value);
}
void NetworkOpener::publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry)
{
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);
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 NetworkOpener::buildMqttPath(const char* path, char* outPath)
{
int offset = 0;
for(const char& c : _mqttPath)
{
if(c == 0x00)
{
break;
}
outPath[offset] = c;
++offset;
}
int i=0;
while(path[i] != 0x00)
{
outPath[offset] = path[i];
++i;
++offset;
}
outPath[offset] = 0x00;
}
void NetworkOpener::subscribe(const char *path)
{
char prefixedPath[500];
buildMqttPath(path, prefixedPath);
_network->subscribe(prefixedPath, MQTT_QOS_LEVEL);
}
bool NetworkOpener::comparePrefixedPath(const char *fullPath, const char *subPath)
{
char prefixedPath[500];
buildMqttPath(subPath, prefixedPath);
return strcmp(fullPath, prefixedPath) == 0;
}
String NetworkOpener::concat(String a, String b)
{
String c = a;
c.concat(b);
return c;
}
bool NetworkOpener::reconnected()
{
bool r = _reconnected;
_reconnected = false;
return r;
}
uint8_t NetworkOpener::queryCommands()
{
uint8_t qc = _queryCommands;
_queryCommands = 0;
return qc;
}

103
src/NetworkOpener.h Normal file
View File

@@ -0,0 +1,103 @@
#pragma once
#include "networkDevices/NetworkDevice.h"
#include "networkDevices/WifiDevice.h"
#include "networkDevices/W5500Device.h"
#include <Preferences.h>
#include <vector>
#include "NukiConstants.h"
#include "NukiOpenerConstants.h"
#include "NetworkLock.h"
class NetworkOpener : public MqttReceiver
{
public:
explicit NetworkOpener(Network* network, Preferences* preferences, char* buffer, size_t bufferSize);
virtual ~NetworkOpener() = default;
void initialize();
void update();
void publishKeyTurnerState(const NukiOpener::OpenerState& keyTurnerState, const NukiOpener::OpenerState& lastKeyTurnerState);
void publishRing(const bool locked);
void publishState(NukiOpener::OpenerState lockState);
void publishAuthorizationInfo(const std::list<NukiOpener::LogEntry>& logEntries);
void clearAuthorizationInfo();
void publishCommandResult(const char* resultStr);
void publishLockstateCommandResult(const char* resultStr);
void publishBatteryReport(const NukiOpener::BatteryReport& batteryReport);
void publishConfig(const NukiOpener::Config& config);
void publishAdvancedConfig(const NukiOpener::AdvancedConfig& config);
void publishRssi(const int& rssi);
void publishRetry(const std::string& message);
void publishBleAddress(const std::string& address);
void publishHASSConfig(char* deviceType, const char* baseTopic, char* name, char* uidString, 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 publishKeypadCommandResult(const char* result);
void publishKeypadJsonCommandResult(const char* result);
void publishTimeControlCommandResult(const char* result);
void setLockActionReceivedCallback(LockActionResult (*lockActionReceivedCallback)(const char* value));
void setConfigUpdateReceivedCallback(void (*configUpdateReceivedCallback)(const char* path, 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;
bool reconnected();
uint8_t queryCommands();
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 publishKeypadEntry(const String topic, NukiLock::KeypadEntry entry);
void buildMqttPath(const char* path, char* outPath);
void subscribe(const char* path);
void logactionCompletionStatusToString(uint8_t value, char* out);
String concat(String a, String b);
Preferences* _preferences;
Network* _network = nullptr;
char _mqttPath[181] = {0};
bool _isConnected = false;
std::vector<char*> _configTopics;
bool _firstTunerStatePublish = true;
bool _haEnabled= false;
bool _reconnected = false;
String _keypadCommandName = "";
String _keypadCommandCode = "";
uint _keypadCommandId = 0;
int _keypadCommandEnabled = 1;
unsigned long _resetRingStateTs = 0;
uint8_t _queryCommands = 0;
uint32_t _authId = 0;
char _authName[33];
bool _authFound = false;
NukiOpener::LockState _currentLockState = NukiOpener::LockState::Undefined;
char* _buffer;
const size_t _bufferSize;
LockActionResult (*_lockActionReceivedCallback)(const char* value) = nullptr;
void (*_configUpdateReceivedCallback)(const char* path, 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;
void (*_timeControlCommandReceivedReceivedCallback)(const char* value) = nullptr;
};

44
src/NukiDeviceId.cpp Normal file
View File

@@ -0,0 +1,44 @@
#include <cstring>
#include <Arduino.h>
#include "NukiDeviceId.h"
#include "PreferencesKeys.h"
NukiDeviceId::NukiDeviceId(Preferences* preferences, const std::string& preferencesId)
: _preferences(preferences),
_preferencesId(preferencesId)
{
_deviceId = _preferences->getUInt(_preferencesId.c_str());
if(_deviceId == 0)
{
assignNewId();
}
}
uint32_t NukiDeviceId::get()
{
return _deviceId;
}
void NukiDeviceId::assignId(const uint32_t& id)
{
_deviceId = id;
_preferences->putUInt(_preferencesId.c_str(), id);
}
void NukiDeviceId::assignNewId()
{
assignId(getRandomId());
}
uint32_t NukiDeviceId::getRandomId()
{
uint8_t rnd[4];
for(int i=0; i<4; i++)
{
rnd[i] = random(255);
}
uint32_t deviceId;
memcpy(&deviceId, &rnd, sizeof(deviceId));
return deviceId;
}

22
src/NukiDeviceId.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include <cstdint>
#include <Preferences.h>
class NukiDeviceId
{
public:
NukiDeviceId(Preferences* preferences, const std::string& preferencesId);
uint32_t get();
void assignId(const uint32_t& id);
void assignNewId();
private:
uint32_t getRandomId();
Preferences* _preferences;
const std::string _preferencesId;
uint32_t _deviceId = 0;
};

1471
src/NukiOpenerWrapper.cpp Normal file

File diff suppressed because it is too large Load Diff

134
src/NukiOpenerWrapper.h Normal file
View File

@@ -0,0 +1,134 @@
#pragma once
#include "NukiOpener.h"
#include "NetworkOpener.h"
#include "NukiOpenerConstants.h"
#include "NukiDataTypes.h"
#include "BleScanner.h"
#include "Gpio.h"
#include "NukiDeviceId.h"
class NukiOpenerWrapper : public NukiOpener::SmartlockEventHandler
{
public:
NukiOpenerWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NetworkOpener* network, Gpio* gpio, Preferences* preferences);
virtual ~NukiOpenerWrapper();
void initialize();
void update();
void electricStrikeActuation();
void activateRTO();
void activateCM();
void deactivateRtoCm();
void deactivateRTO();
void deactivateCM();
bool isPinSet();
void setPin(const uint16_t pin);
void unpair();
void disableHASS();
void disableWatchdog();
const NukiOpener::OpenerState& keyTurnerState();
const bool isPaired() const;
const bool hasKeypad() const;
const BLEAddress getBleAddress() const;
std::string firmwareVersion() const;
std::string hardwareVersion() const;
BleScanner::Scanner* bleScanner();
void notify(NukiOpener::EventType eventType) override;
private:
static LockActionResult onLockActionReceivedCallback(const char* value);
static void onConfigUpdateReceivedCallback(const char* topic, 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 onConfigUpdateReceived(const char* topic, const char* value);
void onKeypadCommandReceived(const char* command, const uint& id, const String& name, const String& code, const int& enabled);
void onKeypadJsonCommandReceived(const char* value);
void onTimeControlCommandReceived(const char* value);
void updateKeyTurnerState();
void updateBatteryState();
void updateConfig();
void updateAuthData();
void updateKeypad();
void updateTimeControl(bool retrieved);
void postponeBleWatchdog();
void updateGpioOutputs();
void readConfig();
void readAdvancedConfig();
void setupHASS();
void printCommandResult(Nuki::CmdResult result);
NukiOpener::LockAction lockActionToEnum(const char* str); // char array at least 14 characters
std::string _deviceName;
NukiDeviceId* _deviceId = nullptr;
NukiOpener::NukiOpener _nukiOpener;
BleScanner::Scanner* _bleScanner = nullptr;
NetworkOpener* _network = nullptr;
Gpio* _gpio = nullptr;
Preferences* _preferences = nullptr;
int _intervalLockstate = 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;
int _nrOfRetries = 0;
int _retryDelay = 0;
int _retryCount = 0;
int _retryConfigCount = 0;
int _retryLockstateCount = 0;
unsigned long _nextRetryTs = 0;
std::vector<uint16_t> _keypadCodeIds;
std::vector<uint8_t> _timeControlIds;
NukiOpener::OpenerState _lastKeyTurnerState;
NukiOpener::OpenerState _keyTurnerState;
NukiOpener::BatteryReport _batteryReport;
NukiOpener::BatteryReport _lastBatteryReport;
NukiOpener::Config _nukiConfig = {0};
NukiOpener::AdvancedConfig _nukiAdvancedConfig = {0};
bool _nukiConfigValid = false;
bool _nukiAdvancedConfigValid = false;
bool _hassEnabled = false;
bool _hassSetupCompleted = false;
bool _paired = false;
bool _statusUpdated = false;
bool _hasKeypad = false;
bool _keypadEnabled = false;
uint _maxKeypadCodeCount = 0;
bool _configRead = false;
long _rssiPublishInterval = 0;
unsigned long _nextLockStateUpdateTs = 0;
unsigned long _nextBatteryReportTs = 0;
unsigned long _nextConfigUpdateTs = 0;
unsigned long _nextTimeControlUpdateTs = 0;
unsigned long _nextKeypadUpdateTs = 0;
unsigned long _nextPairTs = 0;
long _nextRssiTs = 0;
unsigned long _lastRssi = 0;
unsigned long _disableBleWatchdogTs = 0;
std::string _firmwareVersion = "";
std::string _hardwareVersion = "";
NukiOpener::LockAction _nextLockAction = (NukiOpener::LockAction)0xff;
};

1459
src/NukiWrapper.cpp Normal file

File diff suppressed because it is too large Load Diff

132
src/NukiWrapper.h Normal file
View File

@@ -0,0 +1,132 @@
#pragma once
#include "NetworkLock.h"
#include "NukiConstants.h"
#include "NukiDataTypes.h"
#include "BleScanner.h"
#include "NukiLock.h"
#include "Gpio.h"
#include "LockActionResult.h"
#include "NukiDeviceId.h"
class NukiWrapper : public Nuki::SmartlockEventHandler
{
public:
NukiWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NetworkLock* network, Gpio* gpio, Preferences* preferences);
virtual ~NukiWrapper();
void initialize(const bool& firstStart);
void update();
void lock();
void unlock();
void unlatch();
void lockngo();
void lockngounlatch();
bool isPinSet();
void setPin(const uint16_t pin);
void unpair();
void disableHASS();
void disableWatchdog();
const NukiLock::KeyTurnerState& keyTurnerState();
const bool isPaired() const;
const bool hasKeypad() const;
bool hasDoorSensor() const;
const BLEAddress getBleAddress() const;
std::string firmwareVersion() const;
std::string hardwareVersion() const;
void notify(Nuki::EventType eventType) override;
private:
static LockActionResult onLockActionReceivedCallback(const char* value);
static void onConfigUpdateReceivedCallback(const char* topic, 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 onConfigUpdateReceived(const char* topic, const char* value);
void onKeypadCommandReceived(const char* command, const uint& id, const String& name, const String& code, const int& enabled);
void onKeypadJsonCommandReceived(const char* value);
void onTimeControlCommandReceived(const char* value);
void updateKeyTurnerState();
void updateBatteryState();
void updateConfig();
void updateAuthData();
void updateKeypad();
void updateTimeControl(bool retrieved);
void postponeBleWatchdog();
void updateGpioOutputs();
void readConfig();
void readAdvancedConfig();
void setupHASS();
void printCommandResult(Nuki::CmdResult result);
NukiLock::LockAction lockActionToEnum(const char* str); // char array at least 14 characters
std::string _deviceName;
NukiDeviceId* _deviceId = nullptr;
NukiLock::NukiLock _nukiLock;
BleScanner::Scanner* _bleScanner = nullptr;
NetworkLock* _network = nullptr;
Gpio* _gpio = nullptr;
Preferences* _preferences;
int _intervalLockstate = 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;
std::vector<uint16_t> _keypadCodeIds;
std::vector<uint8_t> _timeControlIds;
NukiLock::KeyTurnerState _lastKeyTurnerState;
NukiLock::KeyTurnerState _keyTurnerState;
NukiLock::BatteryReport _batteryReport;
NukiLock::BatteryReport _lastBatteryReport;
NukiLock::Config _nukiConfig = {0};
NukiLock::AdvancedConfig _nukiAdvancedConfig = {0};
bool _nukiConfigValid = false;
bool _nukiAdvancedConfigValid = false;
bool _hassEnabled = false;
bool _hassSetupCompleted = false;
bool _paired = false;
bool _statusUpdated = false;
bool _hasKeypad = false;
bool _keypadEnabled = false;
uint _maxKeypadCodeCount = 0;
bool _configRead = false;
int _nrOfRetries = 0;
int _retryDelay = 0;
int _retryCount = 0;
int _retryConfigCount = 0;
int _retryLockstateCount = 0;
long _rssiPublishInterval = 0;
unsigned long _nextRetryTs = 0;
unsigned long _nextLockStateUpdateTs = 0;
unsigned long _nextBatteryReportTs = 0;
unsigned long _nextConfigUpdateTs = 0;
unsigned long _nextTimeControlUpdateTs = 0;
unsigned long _nextKeypadUpdateTs = 0;
unsigned long _nextRssiTs = 0;
unsigned long _lastRssi = 0;
unsigned long _disableBleWatchdogTs = 0;
std::string _firmwareVersion = "";
std::string _hardwareVersion = "";
volatile NukiLock::LockAction _nextLockAction = (NukiLock::LockAction)0xff;
};

52
src/Ota.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include <Arduino.h>
#include "Ota.h"
#include "Logger.h"
#include "RestartReason.h"
#define FULL_PACKET 1436 // HTTP_UPLOAD_BUFLEN in WebServer,h
void Ota::updateFirmware(uint8_t* buf, size_t size)
{
if(!_updateStarted && size == 0)
{
Log->println("OTA upload cancelled, size is 0.");
return;
}
if (!_updateStarted)
{ //If it's the first packet of OTA since bootup, begin OTA
Log->println("BeginOTA");
esp_ota_begin(esp_ota_get_next_update_partition(NULL), OTA_SIZE_UNKNOWN, &otaHandler);
_updateStarted = true;
}
esp_ota_write(otaHandler, buf, size);
if (size != FULL_PACKET)
{
esp_ota_end(otaHandler);
Log->println("EndOTA");
if (ESP_OK == esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL)))
{
_updateCompleted = true;
}
else
{
Log->println("Upload Error");
}
}
}
bool Ota::updateStarted()
{
return _updateStarted;
}
bool Ota::updateCompleted()
{
return _updateCompleted;
}
void Ota::restart()
{
_updateCompleted = false;
_updateStarted = false;
}

20
src/Ota.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <stdint.h>
#include <cstddef>
#include "esp_ota_ops.h"
class Ota
{
public:
void updateFirmware(uint8_t* buf, size_t size);
bool updateStarted();
bool updateCompleted();
void restart();
private:
bool _updateStarted = false;
bool _updateCompleted = false;
esp_ota_handle_t otaHandler = 0;
};

261
src/PreferencesKeys.h Normal file
View File

@@ -0,0 +1,261 @@
#pragma once
#include <vector>
#define preference_started_before "run"
#define preference_config_version "confVersion"
#define preference_device_id_lock "deviceId"
#define preference_device_id_opener "deviceIdOp"
#define preference_nuki_id_lock "nukiId"
#define preference_nuki_id_opener "nukidOp"
#define preference_mqtt_broker "mqttbroker"
#define preference_mqtt_broker_port "mqttport"
#define preference_mqtt_user "mqttuser"
#define preference_mqtt_password "mqttpass"
#define preference_mqtt_log_enabled "mqttlog"
#define preference_lock_enabled "lockena"
#define preference_lock_pin_status "lockpin"
#define preference_mqtt_lock_path "mqttpath"
#define preference_opener_enabled "openerena"
#define preference_opener_pin_status "openerpin"
#define preference_opener_continuous_mode "openercont"
#define preference_mqtt_opener_path "mqttoppath"
#define preference_check_updates "checkupdates"
#define preference_lock_max_keypad_code_count "maxkpad"
#define preference_opener_max_keypad_code_count "opmaxkpad"
#define preference_mqtt_ca "mqttca"
#define preference_mqtt_crt "mqttcrt"
#define preference_mqtt_key "mqttkey"
#define preference_mqtt_hass_discovery "hassdiscovery"
#define preference_mqtt_hass_cu_url "hassConfigUrl"
#define preference_ip_dhcp_enabled "dhcpena"
#define preference_ip_address "ipaddr"
#define preference_ip_subnet "ipsub"
#define preference_ip_gateway "ipgtw"
#define preference_ip_dns_server "dnssrv"
#define preference_network_hardware "nwhw"
#define preference_network_hardware_gpio "nwhwdt" // obsolete
#define preference_network_wifi_fallback_disabled "nwwififb"
#define preference_find_best_rssi "nwbestrssi"
#define preference_rssi_publish_interval "rssipb"
#define preference_hostname "hostname"
#define preference_network_timeout "nettmout"
#define preference_restart_on_disconnect "restdisc"
#define preference_restart_timer "resttmr"
#define preference_restart_ble_beacon_lost "rstbcn"
#define preference_query_interval_lockstate "lockStInterval"
#define preference_query_interval_configuration "configInterval"
#define preference_query_interval_battery "batInterval"
#define preference_query_interval_keypad "kpInterval"
#define preference_access_level "accLvl"
#define preference_admin_enabled "aclConfig"
#define preference_keypad_info_enabled "kpInfoEnabled"
#define preference_keypad_control_enabled "kpCntrlEnabled"
#define preference_timecontrol_control_enabled "tcCntrlEnabled"
#define preference_timecontrol_info_enabled "tcInfoEnabled"
#define preference_publish_authdata "pubAuth"
#define preference_acl "aclLckOpn"
#define preference_register_as_app "regAsApp" // true = register as hub; false = register as app
#define preference_command_nr_of_retries "nrRetry"
#define preference_command_retry_delay "rtryDelay"
#define preference_cred_user "crdusr"
#define preference_cred_password "crdpass"
#define preference_gpio_locking_enabled "gpiolck" // obsolete
#define preference_gpio_configuration "gpiocfg"
#define preference_publish_debug_info "pubdbg"
#define preference_presence_detection_timeout "prdtimeout"
#define preference_has_mac_saved "hasmac"
#define preference_has_mac_byte_0 "macb0"
#define preference_has_mac_byte_1 "macb1"
#define preference_has_mac_byte_2 "macb2"
#define preference_latest_version "latest"
class DebugPreferences
{
private:
std::vector<char*> _keys =
{
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_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_mqtt_ca, preference_mqtt_crt, preference_mqtt_key, preference_mqtt_hass_discovery, preference_mqtt_hass_cu_url,
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_find_best_rssi, preference_hostname, 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_admin_enabled, preference_keypad_info_enabled, preference_acl,
preference_timecontrol_control_enabled, preference_timecontrol_info_enabled,
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_has_mac_saved, preference_has_mac_byte_0, preference_has_mac_byte_1, preference_has_mac_byte_2, preference_latest_version,
};
std::vector<char*> _redact =
{
preference_mqtt_user, preference_mqtt_password,
preference_mqtt_ca, preference_mqtt_crt, preference_mqtt_key,
preference_cred_user, preference_cred_password,
preference_nuki_id_lock, preference_nuki_id_opener,
};
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_find_best_rssi, preference_restart_on_disconnect, preference_keypad_control_enabled, preference_admin_enabled, preference_keypad_info_enabled,
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
};
const bool isRedacted(const char* key) const
{
return std::find(_redact.begin(), _redact.end(), key) != _redact.end();
}
const String redact(const String s) const
{
return s == "" ? "" : "***";
}
const String redact(const int32_t i) const
{
return i == 0 ? "" : "***";
}
const String redact(const uint32_t i) const
{
return i == 0 ? "" : "***";
}
const String redact(const int64_t i) const
{
return i == 0 ? "" : "***";
}
const String redact(const uint64_t i) const
{
return i == 0 ? "" : "***";
}
const void appendPreferenceInt8(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(isRedacted(key) ? redact((const int32_t)preferences->getChar(key)) : String(preferences->getChar(key)));
s.concat("\n");
}
const void appendPreferenceUInt8(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(isRedacted(key) ? redact((const uint32_t)preferences->getUChar(key)) : String(preferences->getUChar(key)));
s.concat("\n");
}
const void appendPreferenceInt16(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(isRedacted(key) ? redact((const int32_t)preferences->getShort(key)) : String(preferences->getShort(key)));
s.concat("\n");
}
const void appendPreferenceUInt16(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(isRedacted(key) ? redact((const uint32_t)preferences->getUShort(key)) : String(preferences->getUShort(key)));
s.concat("\n");
}
const void appendPreferenceInt32(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(isRedacted(key) ? redact((const int32_t)preferences->getInt(key)) : String(preferences->getInt(key)));
s.concat("\n");
}
const void appendPreferenceUInt32(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(isRedacted(key) ? redact((const uint32_t)preferences->getUInt(key)) : String(preferences->getUInt(key)));
s.concat("\n");
}
const void appendPreferenceInt64(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(isRedacted(key) ? redact((const int64_t)preferences->getLong64(key)) : String(preferences->getLong64(key)));
s.concat("\n");
}
const void appendPreferenceUInt64(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(isRedacted(key) ? redact((const uint64_t)preferences->getULong64(key)) : String(preferences->getULong64(key)));
s.concat("\n");
}
const void appendPreferenceBool(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(preferences->getBool(key) ? "true" : "false");
s.concat("\n");
}
const void appendPreferenceString(Preferences *preferences, String& s, const char* description, const char* key)
{
s.concat(description);
s.concat(": ");
s.concat(isRedacted(key) ? redact((const String)preferences->getString(key)) : preferences->getString(key));
s.concat("\n");
}
const void appendPreference(Preferences *preferences, String& s, const char* key)
{
if(std::find(_boolPrefs.begin(), _boolPrefs.end(), key) != _boolPrefs.end())
{
appendPreferenceBool(preferences, s, key, key);
return;
}
switch(preferences->getType(key))
{
case PT_I8:
appendPreferenceInt8(preferences, s, key, key);
break;
case PT_I16:
appendPreferenceInt16(preferences, s, key, key);
break;
case PT_I32:
appendPreferenceInt32(preferences, s, key, key);
break;
case PT_I64:
appendPreferenceInt64(preferences, s, key, key);
break;
case PT_U8:
appendPreferenceUInt8(preferences, s, key, key);
break;
case PT_U16:
appendPreferenceUInt16(preferences, s, key, key);
break;
case PT_U32:
appendPreferenceUInt32(preferences, s, key, key);
break;
case PT_U64:
appendPreferenceUInt64(preferences, s, key, key);
break;
case PT_STR:
appendPreferenceString(preferences, s, key, key);
break;
default:
appendPreferenceString(preferences, s, key, key);
break;
}
}
public:
const String preferencesToString(Preferences *preferences)
{
String s = "";
for(const auto& key : _keys)
{
appendPreference(preferences, s, key);
}
return s;
}
};

223
src/PresenceDetection.cpp Normal file
View File

@@ -0,0 +1,223 @@
#include "PresenceDetection.h"
#include "PreferencesKeys.h"
#include "Logger.h"
#include "CharBuffer.h"
#include <NimBLEDevice.h>
#include <NimBLEAdvertisedDevice.h>
#include "NimBLEBeacon.h"
#include "NukiUtils.h"
PresenceDetection::PresenceDetection(Preferences* preferences, BleScanner::Scanner *bleScanner, Network* network, char* buffer, size_t bufferSize)
: _preferences(preferences),
_bleScanner(bleScanner),
_network(network),
_csv(buffer),
_bufferSize(bufferSize)
{
_timeout = _preferences->getInt(preference_presence_detection_timeout) * 1000;
if(_timeout == 0)
{
_timeout = 60000;
_preferences->putInt(preference_presence_detection_timeout, 60);
}
Log->print(F("Presence detection timeout (ms): "));
Log->println(_timeout);
}
PresenceDetection::~PresenceDetection()
{
_bleScanner->unsubscribe(this);
_bleScanner = nullptr;
_network = nullptr;
delete _csv;
_csv = nullptr;
}
void PresenceDetection::initialize()
{
_bleScanner->subscribe(this);
}
void PresenceDetection::update()
{
delay(3000);
if(_timeout < 0) return;
memset(_csv, 0, _bufferSize);
if(_devices.size() == 0)
{
strcpy(_csv, ";;");
_network->publishPresenceDetection(_csv);
return;
}
_csvIndex = 0;
long ts = millis();
for(auto it : _devices)
{
if(ts - _timeout < it.second.timestamp)
{
buildCsv(it.second);
}
// Prevent csv buffer overflow
if(_csvIndex > _bufferSize - (sizeof(it.second.name) + sizeof(it.second.address) + 10))
{
break;
}
}
_csv[_csvIndex-1] = 0x00;
// Log->print("Devices found: ");
// Log->println(_devices.size());
_network->publishPresenceDetection(_csv);
}
void PresenceDetection::buildCsv(const PdDevice &device)
{
for(int i = 0; i < 17; i++)
{
_csv[_csvIndex] = device.address[i];
++_csvIndex;
}
_csv[_csvIndex] = ';';
++_csvIndex;
int i=0;
while(device.name[i] != 0x00 && i < sizeof(device.name))
{
_csv[_csvIndex] = device.name[i];
++_csvIndex;
++i;
}
_csv[_csvIndex] = ';';
++_csvIndex;
if(device.hasRssi)
{
char rssiStr[20] = {0};
itoa(device.rssi, rssiStr, 10);
int i=0;
while(rssiStr[i] != 0x00 && i < 20)
{
_csv[_csvIndex] = rssiStr[i];
++_csvIndex;
++i;
}
}
_csv[_csvIndex] = '\n';
_csvIndex++;
}
void PresenceDetection::onResult(NimBLEAdvertisedDevice *device)
{
std::string addressStr = device->getAddress().toString();
char addrArrComp[13] = {0};
// Log->println(addressStr.c_str());
addrArrComp[0] = addressStr.at(0);
addrArrComp[1] = addressStr.at(1);
addrArrComp[2] = addressStr.at(3);
addrArrComp[3] = addressStr.at(4);
addrArrComp[4] = addressStr.at(6);
addrArrComp[5] = addressStr.at(7);
addrArrComp[6] = addressStr.at(9);
addrArrComp[7] = addressStr.at(10);
addrArrComp[8] = addressStr.at(12);
addrArrComp[9] = addressStr.at(13);
addrArrComp[10] = addressStr.at(15);
addrArrComp[11] = addressStr.at(16);
long long addr = strtoll(addrArrComp, nullptr, 16);
auto it = _devices.find(addr);
if(it == _devices.end())
{
PdDevice pdDevice;
int i=0;
size_t len = addressStr.length();
while(i < len)
{
pdDevice.address[i] = addressStr.at(i);
++i;
}
if(device->haveRSSI())
{
pdDevice.hasRssi = true;
pdDevice.rssi = device->getRSSI();
}
std::string nameStr = "-";
if(device->haveName())
{
std::string nameStr = device->getName();
i=0;
len = nameStr.length();
while(i < len && i < sizeof(pdDevice.name)-1)
{
pdDevice.name[i] = nameStr.at(i);
++i;
}
pdDevice.timestamp = millis();
_devices[addr] = pdDevice;
}
else if (device->haveManufacturerData())
{
std::string strManufacturerData = device->getManufacturerData();
uint8_t cManufacturerData[100];
strManufacturerData.copy((char *)cManufacturerData, std::min(strManufacturerData.length(), sizeof(cManufacturerData)), 0);
if (strManufacturerData.length() == 25 && cManufacturerData[0] == 0x4C && cManufacturerData[1] == 0x00)
{
BLEBeacon oBeacon = BLEBeacon();
oBeacon.setData(strManufacturerData);
if(ENDIAN_CHANGE_U16(oBeacon.getMinor()) == 40004)
{
pdDevice.timestamp = millis();
strcpy(pdDevice.name, oBeacon.getProximityUUID().toString().c_str());
_devices[addr] = pdDevice;
}
}
}
}
else
{
it->second.timestamp = millis();
if(device->haveRSSI())
{
it->second.hasRssi = true;
it->second.rssi = device->getRSSI();
}
}
// if(device->haveName())
// {
// Log->print(" | ");
// Log->print(device->getName().c_str());
// if(device->haveRSSI())
// {
// Log->print(" | ");
// Log->print(device->getRSSI());
// }
// }
// Log->println();
}

38
src/PresenceDetection.h Normal file
View File

@@ -0,0 +1,38 @@
#pragma once
#include "BleScanner.h"
#include "BleInterfaces.h"
#include "Network.h"
struct PdDevice
{
char address[18] = {0};
char name[37] = {0};
unsigned long timestamp = 0;
int rssi = 0;
bool hasRssi = false;
};
class PresenceDetection : public BleScanner::Subscriber
{
public:
PresenceDetection(Preferences* preferences, BleScanner::Scanner* bleScanner, Network* network, char* buffer, size_t bufferSize);
virtual ~PresenceDetection();
void initialize();
void update();
void onResult(NimBLEAdvertisedDevice* advertisedDevice) override;
private:
void buildCsv(const PdDevice& device);
Preferences* _preferences;
BleScanner::Scanner* _bleScanner;
Network* _network;
char* _csv = {0};
size_t _bufferSize = 0;
std::map<long long, PdDevice> _devices;
int _timeout = 20000;
int _csvIndex = 0;
};

6
src/QueryCommand.h Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
#define QUERY_COMMAND_LOCKSTATE 1
#define QUERY_COMMAND_CONFIG 2
#define QUERY_COMMAND_KEYPAD 4
#define QUERY_COMMAND_BATTERY 8

147
src/RestartReason.h Normal file
View File

@@ -0,0 +1,147 @@
#pragma once
enum class RestartReason
{
RequestedViaMqtt,
BLEBeaconWatchdog,
RestartOnDisconnectWatchdog,
RestartIntervalWatchdog,
NetworkTimeoutWatchdog,
WifiInitFailed,
ReconfigureWifi,
ReconfigureLAN8720,
NetworkDeviceCriticalFailure,
NetworkDeviceCriticalFailureNoWifiFallback,
ConfigurationUpdated,
GpioConfigurationUpdated,
RestartTimer,
OTACompleted,
OTATimeout,
OTAAborted,
OTAUnknownState,
DeviceUnpaired,
NotApplicable
};
#define RESTART_REASON_VALID_DETECT 0xa00ab00bc00bd00d;
extern int restartReason;
extern uint64_t restartReasonValidDetect;
extern bool rebuildGpioRequested;
extern RestartReason currentRestartReason;
extern bool restartReason_isValid;
inline static void restartEsp(RestartReason reason)
{
if(reason == RestartReason::GpioConfigurationUpdated)
{
rebuildGpioRequested = true;
}
restartReason = (int)reason;
restartReasonValidDetect = RESTART_REASON_VALID_DETECT;
ESP.restart();
}
inline static void initializeRestartReason()
{
uint64_t cmp = RESTART_REASON_VALID_DETECT;
restartReason_isValid = (restartReasonValidDetect == cmp);
if(restartReason_isValid)
{
currentRestartReason = (RestartReason)restartReason;
memset(&restartReasonValidDetect, 0, sizeof(restartReasonValidDetect));
}
else
{
rebuildGpioRequested = false;
}
}
inline static String getRestartReason()
{
switch(currentRestartReason)
{
case RestartReason::RequestedViaMqtt:
return "RequestedViaMqtt";
case RestartReason::BLEBeaconWatchdog:
return "BLEBeaconWatchdog";
case RestartReason::RestartOnDisconnectWatchdog:
return "RestartOnDisconnectWatchdog";
case RestartReason::RestartIntervalWatchdog:
return "RestartIntervalWatchdog";
case RestartReason::NetworkTimeoutWatchdog:
return "NetworkTimeoutWatchdog";
case RestartReason::WifiInitFailed:
return "WifiInitFailed";
case RestartReason::ReconfigureWifi:
return "ReconfigureWifi";
case RestartReason::ReconfigureLAN8720:
return "ReconfigureLAN8720";
case RestartReason::NetworkDeviceCriticalFailure:
return "NetworkDeviceCriticalFailure";
case RestartReason::NetworkDeviceCriticalFailureNoWifiFallback:
return "NetworkDeviceCriticalFailureNoWifiFallback";
case RestartReason::ConfigurationUpdated:
return "ConfigurationUpdated";
case RestartReason::GpioConfigurationUpdated:
return "GpioConfigurationUpdated";
case RestartReason::RestartTimer:
return "RestartTimer";
case RestartReason::OTACompleted:
return "OTACompleted";
case RestartReason::OTATimeout:
return "OTATimeout";
case RestartReason::OTAAborted:
return "OTAAborted";
case RestartReason::OTAUnknownState:
return "OTAUnknownState";
case RestartReason::DeviceUnpaired:
return "DeviceUnpaired";
case RestartReason::NotApplicable:
return "NotApplicable";
default:
return "Unknown: " + restartReason;
}
}
inline static String getEspRestartReason()
{
esp_reset_reason_t reason = esp_reset_reason();
switch(reason)
{
case esp_reset_reason_t::ESP_RST_UNKNOWN:
return "ESP_RST_UNKNOWN: Reset reason can not be determined.";
case esp_reset_reason_t::ESP_RST_POWERON:
return "ESP_RST_POWERON: Reset due to power-on event.";
case esp_reset_reason_t::ESP_RST_EXT:
return "ESP_RST_EXT: Reset by external pin";
case esp_reset_reason_t::ESP_RST_SW:
return "ESP_RST_SW: Software reset via esp_restart.";
case esp_reset_reason_t::ESP_RST_PANIC:
return "ESP_RST_PANIC: Software reset due to exception/panic.";
case esp_reset_reason_t::ESP_RST_INT_WDT:
return "ESP_RST_INT_WDT: Reset (software or hardware) due to interrupt watchdog";
case esp_reset_reason_t::ESP_RST_TASK_WDT:
return "ESP_RST_TASK_WDT: Reset due to task watchdog.";
case esp_reset_reason_t::ESP_RST_WDT:
return "ESP_RST_WDT: Reset due to other watchdogs.";
case esp_reset_reason_t::ESP_RST_DEEPSLEEP:
return "ESP_RST_DEEPSLEEP: Reset after exiting deep sleep mode.";
case esp_reset_reason_t::ESP_RST_BROWNOUT:
return "ESP_RST_BROWNOUT: Brownout reset (software or hardware)";
case esp_reset_reason_t::ESP_RST_SDIO:
return "ESP_RST_SDIO: Reset over SDIO.";
default:
return "Unknown: " + (int)reason;
}
}
inline bool rebuildGpio()
{
bool rebGpio = rebuildGpioRequested;
rebuildGpioRequested = false;
return restartReason_isValid && rebGpio;
}

1615
src/WebCfgServer.cpp Normal file

File diff suppressed because it is too large Load Diff

93
src/WebCfgServer.h Normal file
View File

@@ -0,0 +1,93 @@
#pragma once
#include <Preferences.h>
#include <WebServer.h>
#include "NukiWrapper.h"
#include "NetworkLock.h"
#include "NukiOpenerWrapper.h"
#include "Ota.h"
#include "Gpio.h"
extern TaskHandle_t networkTaskHandle;
extern TaskHandle_t nukiTaskHandle;
extern TaskHandle_t presenceDetectionTaskHandle;
enum class TokenType
{
None,
MqttServer,
MqttPort,
MqttUser,
MqttPass,
MqttPath,
QueryIntervalLockstate,
QueryIntervalBattery,
};
class WebCfgServer
{
public:
WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Network* network, Gpio* gpio, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal);
~WebCfgServer() = default;
void initialize();
void update();
private:
bool processArgs(String& message);
void processGpioArgs();
void buildHtml(String& response);
void buildAccLvlHtml(String& response);
void buildCredHtml(String& response);
void buildOtaHtml(String& response, bool errored);
void buildOtaCompletedHtml(String& response);
void buildMqttConfigHtml(String& response);
void buildNukiConfigHtml(String& response);
void buildGpioConfigHtml(String& response);
void buildConfirmHtml(String& response, const String &message, uint32_t redirectDelay = 5);
void buildConfigureWifiHtml(String& response);
void buildInfoHtml(String& response);
void sendCss();
void sendFavicon();
void processUnpair(bool opener);
void buildHtmlHeader(String& response);
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 printCheckBox(String& response, const char* token, const char* description, const bool value);
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);
void buildNavigationButton(String& response, const char* caption, const char* targetPath, const char* labelText = "");
const std::vector<std::pair<String, String>> getNetworkDetectionOptions() const;
const std::vector<std::pair<String, String>> getGpioOptions() const;
String getPreselectionForGpio(const uint8_t& pin);
void printParameter(String& response, const char* description, const char* value, const char *link = "");
String generateConfirmCode();
void waitAndProcess(const bool blocking, const uint32_t duration);
void handleOtaUpload();
WebServer _server;
NukiWrapper* _nuki = nullptr;
NukiOpenerWrapper* _nukiOpener = nullptr;
Network* _network = nullptr;
Gpio* _gpio = nullptr;
Preferences* _preferences = nullptr;
Ota _ota;
bool _hasCredentials = false;
char _credUser[31] = {0};
char _credPassword[31] = {0};
bool _allowRestartToPortal = false;
bool _pinsConfigured = false;
bool _brokerConfigured = false;
uint32_t _transferredSize = 0;
unsigned long _otaStartTs = 0;
String _hostname;
String _confirmCode = "----";
bool _enabled = true;
};

View File

@@ -0,0 +1,62 @@
#pragma once
// escaped by https://www.cescaper.com/
// source: https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css
const char stylecss[] = ":root{--nc-font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';--nc-font-mono:Consolas,monaco,'Ubuntu Mono','Liberation Mono','Courier New',Courier,monospace;--nc-tx-1:#000000;--nc-tx-2:#1A1A1A;--nc-bg-1:#FFFFFF;--nc-bg-2:#F6F8FA;--nc-bg-3:#E5E7EB;--nc-lk-1:#0070F3;--nc-lk-2:#0366D6;--nc-lk-tx:#FFFFFF;--nc-ac-1:#79FFE1;--nc-ac-tx:#0C4047}@media (prefers-color-scheme:dark){:root{--nc-tx-1:#ffffff;--nc-tx-2:#eeeeee;--nc-bg-1:#000000;--nc-bg-2:#111111;--nc-bg-3:#222222;--nc-lk-1:#3291FF;--nc-lk-2:#0070F3;--nc-lk-tx:#FFFFFF;--nc-ac-1:#7928CA;--nc-ac-tx:#FFFFFF}}*{margin:0;padding:0}address,area,article,aside,audio,blockquote,datalist,details,dl,fieldset,figure,iframe,img,input,meter,nav,ol,optgroup,option,output,p,pre,progress,ruby,section,table,textarea,ul,video{margin-bottom:1rem}button,html,input,select{font-family:var(--nc-font-sans)}body{margin:0 auto;max-width:750px;padding:2rem;border-radius:6px;overflow-x:hidden;word-break:normal;overflow-wrap:anywhere;background:var(--nc-bg-1);color:var(--nc-tx-2);font-size:1.03rem;line-height:1.5}::selection{background:var(--nc-ac-1);color:var(--nc-ac-tx)}h1,h2,h3,h4,h5,h6{line-height:1;color:var(--nc-tx-1);padding-top:.875rem}h1,h2,h3{color:var(--nc-tx-1);padding-bottom:2px;margin-bottom:8px;border-bottom:1px solid var(--nc-bg-2)}h4,h5,h6{margin-bottom:.3rem}h1{font-size:2.25rem}h2{font-size:1.85rem}h3{font-size:1.55rem}h4{font-size:1.25rem}h5{font-size:1rem}h6{font-size:.875rem}a{color:var(--nc-lk-1)}a:hover{color:var(--nc-lk-2)}abbr:hover{cursor:help}blockquote{padding:1.5rem;background:var(--nc-bg-2);border-left:5px solid var(--nc-bg-3)}abbr{cursor:help}blockquote :last-child{padding-bottom:0;margin-bottom:0}header{background:var(--nc-bg-2);border-bottom:1px solid var(--nc-bg-3);padding:2rem 1.5rem;margin:-2rem calc(0px - (50vw - 50%)) 2rem;padding-left:calc(50vw - 50%);padding-right:calc(50vw - 50%)}header h1,header h2,header h3{padding-bottom:0;border-bottom:0}header>:first-child{margin-top:0;padding-top:0}header>:last-child{margin-bottom:0}a button,button,input[type=button],input[type=reset],input[type=submit]{font-size:1rem;display:inline-block;padding:6px 12px;text-align:center;text-decoration:none;white-space:nowrap;background:var(--nc-lk-1);color:var(--nc-lk-tx);border:0;border-radius:4px;box-sizing:border-box;cursor:pointer;color:var(--nc-lk-tx)}a button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{cursor:default;opacity:.5;cursor:not-allowed}.button:focus,.button:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:var(--nc-lk-2)}code,kbd,pre,samp{font-family:var(--nc-font-mono)}code,kbd,pre,samp{background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px;padding:3px 6px;font-size:.9rem}kbd{border-bottom:3px solid var(--nc-bg-3)}pre{padding:1rem 1.4rem;max-width:100%;overflow:auto}pre code{background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}code pre{display:inline;background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}details{padding:.6rem 1rem;background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px}summary{cursor:pointer;font-weight:700}details[open]{padding-bottom:.75rem}details[open] summary{margin-bottom:6px}details[open]>:last-child{margin-bottom:0}dt{font-weight:700}dd::before{content:'→ '}hr{border:0;border-bottom:1px solid var(--nc-bg-3);margin:1rem auto}fieldset{margin-top:1rem;padding:2rem;border:1px solid var(--nc-bg-3);border-radius:4px}legend{padding:0.5rem}table{border-collapse:collapse;width:100%}td,th{border:1px solid var(--nc-bg-3);text-align:left;padding:.5rem}th{background:var(--nc-bg-2)}tr:nth-child(even){background:var(--nc-bg-2)}table caption{font-weight:700;margin-bottom:.5rem}textarea{max-width:100%}ol,ul{padding-left:2rem}li{margin-top:.4rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}mark{padding:3px 6px;background:var(--nc-ac-1);color:var(--nc-ac-tx)}input,select,textarea{padding:6px 12px;margin-bottom:.5rem;background:var(--nc-bg-2);color:var(--nc-tx-2);border:1px solid var(--nc-bg-3);border-radius:4px;box-shadow:none;box-sizing:border-box}img{max-width:100%}td>input{margin-top:0px;margin-bottom:0px}td>textarea{margin-top:0px;margin-bottom:0px}td>select{margin-top:0px;margin-bottom:0px}#tblnav td,th{border:0;border-bottom:1px solid;}.tdbtn{text-align:center;vertical-align: middle;}";
// converted to char array by https://notisrac.github.io/FileToCArray/
const unsigned char favicon_32x32[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x08, 0x06, 0x00, 0x00, 0x00, 0x73, 0x7a, 0x7a,
0xf4, 0x00, 0x00, 0x02, 0xfb, 0x49, 0x44, 0x41, 0x54, 0x58, 0x47, 0xcd, 0x97, 0x4b, 0x4c, 0x53,
0x41, 0x14, 0x86, 0xff, 0x69, 0xa5, 0x3c, 0x8a, 0x12, 0x14, 0x84, 0xa6, 0x96, 0x60, 0x6d, 0xd0,
0x50, 0x14, 0x51, 0xd1, 0x6a, 0x4c, 0x51, 0x63, 0x88, 0x8a, 0x06, 0x63, 0x42, 0xc0, 0x85, 0x01,
0xc3, 0xae, 0xe8, 0x0a, 0x0d, 0x4a, 0x30, 0x31, 0x18, 0xd1, 0x84, 0x18, 0x83, 0x46, 0x5d, 0xa9,
0x09, 0xb8, 0x01, 0xa2, 0x31, 0x82, 0x31, 0x82, 0x6e, 0x48, 0x78, 0xb6, 0x74, 0xe1, 0x8b, 0x47,
0xb0, 0x3e, 0x0a, 0xb4, 0x05, 0xa1, 0xe1, 0x25, 0x14, 0x68, 0xeb, 0x9d, 0x22, 0xb7, 0x40, 0x1f,
0x1b, 0xef, 0xa5, 0xce, 0xea, 0xde, 0x99, 0xce, 0x39, 0xdf, 0xfc, 0x7f, 0x67, 0xee, 0x1c, 0xe2,
0xbc, 0x0e, 0x81, 0xe3, 0x84, 0xba, 0x58, 0x40, 0x88, 0xc6, 0x09, 0x48, 0xb0, 0x0a, 0x8d, 0x00,
0x26, 0x07, 0x71, 0x3e, 0x14, 0xd4, 0x37, 0x95, 0x11, 0x7b, 0x87, 0xba, 0x84, 0x10, 0x72, 0x63,
0x15, 0xf2, 0x7a, 0xa4, 0x70, 0x12, 0xe7, 0x35, 0xe2, 0xe8, 0x48, 0x33, 0x81, 0x20, 0x36, 0x10,
0x00, 0x4c, 0x4e, 0x33, 0x71, 0x68, 0xd3, 0x18, 0xe5, 0x03, 0xd7, 0xfe, 0x6f, 0x80, 0x86, 0xb6,
0x51, 0xd4, 0xbe, 0x1b, 0x66, 0xe5, 0xd1, 0x64, 0x49, 0x91, 0xb2, 0x35, 0xdc, 0xaf, 0x5c, 0x25,
0x8f, 0xbe, 0xc1, 0x36, 0xeb, 0x60, 0x7f, 0x93, 0x79, 0x28, 0x0a, 0x07, 0x93, 0x23, 0x7c, 0xce,
0xf1, 0xab, 0xc0, 0x9d, 0x67, 0x46, 0x5c, 0xae, 0x30, 0xb0, 0x93, 0x93, 0xb6, 0x88, 0xa1, 0xad,
0xdc, 0x85, 0x60, 0x91, 0xc0, 0x67, 0xc0, 0xc8, 0xc3, 0xcd, 0x18, 0x9b, 0x9c, 0x67, 0xc7, 0x2b,
0x2e, 0x29, 0x70, 0x31, 0x5b, 0xca, 0x0d, 0x00, 0x8d, 0x72, 0xf5, 0x7c, 0x1c, 0x6e, 0x6a, 0x36,
0xfb, 0x0c, 0xb8, 0xe1, 0x48, 0x33, 0xac, 0x13, 0x3c, 0x02, 0xac, 0x11, 0x12, 0xb4, 0x3c, 0x49,
0xc1, 0x9e, 0xc4, 0xb5, 0x5e, 0x21, 0xa2, 0x8f, 0xb6, 0x60, 0x64, 0x6c, 0x8e, 0x3f, 0x05, 0x68,
0x64, 0xa5, 0x5c, 0x0c, 0x5d, 0x95, 0x77, 0x2b, 0x62, 0xd2, 0x5b, 0x30, 0x6c, 0xe5, 0x19, 0xc0,
0x9f, 0x15, 0x92, 0x63, 0xad, 0xb0, 0x8c, 0xcc, 0xf2, 0xab, 0x00, 0x8d, 0x4e, 0xad, 0x68, 0x66,
0xac, 0x48, 0x5d, 0x61, 0xc5, 0xa6, 0xe3, 0xad, 0x18, 0xfc, 0xc5, 0x13, 0xc0, 0x3a, 0xb1, 0x10,
0xe3, 0x53, 0x76, 0x76, 0x75, 0x4a, 0x79, 0x18, 0x63, 0xc5, 0xee, 0x65, 0xbb, 0x22, 0x2e, 0xa3,
0x0d, 0xfd, 0x43, 0x36, 0x7e, 0x14, 0x50, 0xa7, 0x44, 0xb8, 0x92, 0x35, 0xb6, 0x5b, 0xd9, 0x04,
0x57, 0xf2, 0x64, 0x28, 0x2b, 0x90, 0xb3, 0xef, 0xf1, 0xa7, 0xda, 0xf0, 0xd3, 0xcc, 0x13, 0x00,
0x3d, 0x84, 0x6a, 0x6e, 0x27, 0x62, 0x47, 0x8e, 0x0e, 0xd3, 0xb6, 0x85, 0xc3, 0xc6, 0x65, 0xc5,
0x63, 0xc6, 0x0a, 0xe5, 0xc2, 0xae, 0x90, 0x67, 0xb6, 0xe3, 0xfb, 0xe0, 0x0c, 0x3f, 0x0a, 0x50,
0xc9, 0x3f, 0x56, 0xa7, 0xa2, 0xbc, 0xd2, 0x88, 0xa2, 0xfb, 0xee, 0x03, 0x2a, 0xf1, 0xaf, 0x15,
0x21, 0x8c, 0x3a, 0x8a, 0xd3, 0xed, 0x30, 0x0c, 0xf0, 0x04, 0xb0, 0x2d, 0x3e, 0x0c, 0x5f, 0x6a,
0x53, 0x31, 0x6f, 0x77, 0x42, 0x95, 0xab, 0x87, 0xbe, 0x67, 0x92, 0x5d, 0x69, 0x51, 0xae, 0x0c,
0xb7, 0x2e, 0xc8, 0x91, 0x70, 0xa6, 0x03, 0x7d, 0xc6, 0x69, 0x7e, 0x14, 0x48, 0x88, 0x0b, 0x45,
0xf7, 0xf3, 0xbd, 0xae, 0xe0, 0xfa, 0xee, 0x49, 0xa8, 0xf2, 0xf4, 0x2e, 0x18, 0xda, 0x84, 0x02,
0xba, 0x2b, 0x76, 0x22, 0xbf, 0xb4, 0x17, 0x9f, 0x0d, 0x53, 0xfc, 0x00, 0x28, 0x64, 0xa1, 0xe8,
0x7d, 0xb1, 0x00, 0x40, 0x5b, 0xd1, 0x3d, 0x03, 0xca, 0xab, 0x8c, 0xec, 0xfb, 0x76, 0x85, 0x18,
0xa2, 0x20, 0x01, 0x3a, 0xbb, 0x26, 0xf8, 0x01, 0x90, 0x4b, 0x43, 0xd0, 0xf7, 0x72, 0x1f, 0x1b,
0xfc, 0xf7, 0x8c, 0x1d, 0xc9, 0x67, 0x3b, 0xf1, 0xb5, 0xdf, 0x2d, 0x39, 0x23, 0x04, 0x1c, 0x4b,
0x6e, 0x18, 0x9c, 0x7e, 0x8c, 0xe2, 0x25, 0x21, 0x30, 0xbc, 0x72, 0x03, 0x50, 0x92, 0xf7, 0x5a,
0x2b, 0xd2, 0x35, 0x1f, 0xe0, 0xeb, 0x56, 0xc3, 0x29, 0x80, 0x2c, 0x36, 0x18, 0x3f, 0xea, 0x54,
0xac, 0x02, 0x8b, 0x0f, 0xf9, 0xa5, 0x3d, 0x78, 0x5a, 0x67, 0xf6, 0xe8, 0xa7, 0x1d, 0x9c, 0x02,
0x48, 0x37, 0x06, 0xc3, 0xf8, 0xda, 0x13, 0x60, 0x74, 0x7c, 0x0e, 0xca, 0x2c, 0x1d, 0x2c, 0xa3,
0xee, 0x23, 0x78, 0x91, 0x86, 0x53, 0x00, 0x49, 0x94, 0x08, 0x03, 0x6f, 0xf6, 0x7b, 0x5d, 0x69,
0x4d, 0xe3, 0x10, 0x72, 0x8a, 0xbb, 0x3c, 0xc6, 0x38, 0x05, 0x88, 0x59, 0x2f, 0x82, 0xe9, 0xad,
0x77, 0x00, 0x9a, 0x39, 0xb3, 0xf0, 0x13, 0xea, 0x9a, 0x46, 0x96, 0x41, 0x70, 0x0a, 0x10, 0x1d,
0x19, 0x04, 0x4b, 0xc3, 0x01, 0xaf, 0x0a, 0xd0, 0xce, 0x7e, 0x8b, 0x0d, 0x49, 0xd9, 0xda, 0x65,
0x1f, 0xac, 0x7f, 0x02, 0x58, 0x79, 0x29, 0x0d, 0x0f, 0x15, 0xe2, 0x6e, 0xa1, 0xc2, 0x27, 0x00,
0x1d, 0xa8, 0x66, 0xac, 0xa8, 0x5f, 0xa2, 0xc2, 0xb9, 0x8c, 0x58, 0xa4, 0xab, 0x22, 0x7d, 0xce,
0xf9, 0xbf, 0xaf, 0xe5, 0x7e, 0x97, 0xca, 0xd1, 0x20, 0x55, 0xc0, 0xc4, 0xc4, 0x0a, 0x5c, 0x69,
0x66, 0xd7, 0x31, 0xc5, 0xa9, 0x33, 0x30, 0xc5, 0x29, 0xa1, 0xc5, 0xa9, 0xab, 0x3c, 0x3f, 0xa9,
0x2e, 0x66, 0x20, 0x0a, 0x56, 0x51, 0x09, 0x33, 0x93, 0xfc, 0x01, 0xf3, 0x6f, 0x2d, 0xfb, 0x03,
0xed, 0x06, 0xb0, 0xce, 0xb5, 0xc4, 0xb4, 0x59, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
0xae, 0x42, 0x60, 0x82
};

301
src/main.cpp Normal file
View File

@@ -0,0 +1,301 @@
#include "Arduino.h"
#include "NukiWrapper.h"
#include "NetworkLock.h"
#include "WebCfgServer.h"
#include <RTOS.h>
#include "PreferencesKeys.h"
#include "PresenceDetection.h"
#include "hardware/W5500EthServer.h"
#include "hardware/WifiEthServer.h"
#include "NukiOpenerWrapper.h"
#include "Gpio.h"
#include "Logger.h"
#include "Config.h"
#include "RestartReason.h"
#include "CharBuffer.h"
#include "NukiDeviceId.h"
Network* network = nullptr;
NetworkLock* networkLock = nullptr;
NetworkOpener* networkOpener = nullptr;
WebCfgServer* webCfgServer = nullptr;
BleScanner::Scanner* bleScanner = nullptr;
NukiWrapper* nuki = nullptr;
NukiOpenerWrapper* nukiOpener = nullptr;
PresenceDetection* presenceDetection = nullptr;
NukiDeviceId* deviceIdLock = nullptr;
NukiDeviceId* deviceIdOpener = nullptr;
Preferences* preferences = nullptr;
EthServer* ethServer = nullptr;
Gpio* gpio = nullptr;
bool lockEnabled = false;
bool openerEnabled = false;
unsigned long restartTs = (2^32) - 5 * 60000;
RTC_NOINIT_ATTR int restartReason;
RTC_NOINIT_ATTR uint64_t restartReasonValidDetect;
RTC_NOINIT_ATTR bool rebuildGpioRequested;
bool restartReason_isValid;
RestartReason currentRestartReason = RestartReason::NotApplicable;
TaskHandle_t networkTaskHandle = nullptr;
TaskHandle_t nukiTaskHandle = nullptr;
TaskHandle_t presenceDetectionTaskHandle = nullptr;
void networkTask(void *pvParameters)
{
while(true)
{
bool connected = network->update();
if(connected && openerEnabled)
{
networkOpener->update();
}
webCfgServer->update();
// millis() is about to overflow. Restart device to prevent problems with overflow
if(millis() > restartTs)
{
Log->println(F("Restart timer expired, restarting device."));
delay(200);
restartEsp(RestartReason::RestartTimer);
}
delay(100);
// if(wmts < millis())
// {
// Serial.print("# ");
// Serial.println(uxTaskGetStackHighWaterMark(NULL));
// wmts = millis() + 60000;
// }
}
}
void nukiTask(void *pvParameters)
{
while(true)
{
bleScanner->update();
delay(20);
bool needsPairing = (lockEnabled && !nuki->isPaired()) || (openerEnabled && !nukiOpener->isPaired());
if (needsPairing)
{
delay(5000);
}
if(lockEnabled)
{
nuki->update();
}
if(openerEnabled)
{
nukiOpener->update();
}
}
}
void presenceDetectionTask(void *pvParameters)
{
while(true)
{
presenceDetection->update();
}
}
void setupTasks()
{
// configMAX_PRIORITIES is 25
xTaskCreatePinnedToCore(networkTask, "ntw", 8192, NULL, 3, &networkTaskHandle, 1);
xTaskCreatePinnedToCore(nukiTask, "nuki", 3328, NULL, 2, &nukiTaskHandle, 1);
xTaskCreatePinnedToCore(presenceDetectionTask, "prdet", 896, NULL, 5, &presenceDetectionTaskHandle, 1);
}
void initEthServer(const NetworkDeviceType device)
{
switch (device)
{
case NetworkDeviceType::W5500:
ethServer = new W5500EthServer(80);
break;
case NetworkDeviceType::WiFi:
ethServer = new WifiEthServer(80);
break;
default:
ethServer = new WifiEthServer(80);
break;
}
}
bool initPreferences()
{
preferences = new Preferences();
preferences->begin("nukihub", false);
// preferences->putBool(preference_network_wifi_fallback_disabled, false);
bool firstStart = !preferences->getBool(preference_started_before);
if(firstStart)
{
preferences->putBool(preference_started_before, true);
preferences->putBool(preference_lock_enabled, true);
preferences->putBool(preference_admin_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));
}
else
{
int configVer = preferences->getInt(preference_config_version);
if(configVer < (atof(NUKI_HUB_VERSION) * 100))
{
if (configVer < 834)
{
if(preferences->getInt(preference_keypad_control_enabled))
{
preferences->putBool(preference_keypad_info_enabled, true);
}
else
{
preferences->putBool(preference_keypad_info_enabled, false);
}
switch(preferences->getInt(preference_access_level))
{
case 0:
{
preferences->putBool(preference_keypad_control_enabled, true);
preferences->putBool(preference_admin_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));
break;
}
case 1:
{
preferences->putBool(preference_keypad_control_enabled, false);
preferences->putBool(preference_admin_enabled, false);
uint32_t aclPrefs[17] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0};
preferences->putBytes(preference_acl, (byte*)(&aclPrefs), sizeof(aclPrefs));
break;
}
case 2:
{
preferences->putBool(preference_keypad_control_enabled, false);
preferences->putBool(preference_admin_enabled, false);
uint32_t aclPrefs[17] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
preferences->putBytes(preference_acl, (byte*)(&aclPrefs), sizeof(aclPrefs));
break;
}
case 3:
{
preferences->putBool(preference_keypad_control_enabled, false);
preferences->putBool(preference_admin_enabled, false);
uint32_t aclPrefs[17] = {1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0};
preferences->putBytes(preference_acl, (byte*)(&aclPrefs), sizeof(aclPrefs));
break;
}
}
}
preferences->putInt(preference_config_version, atof(NUKI_HUB_VERSION) * 100);
}
}
return firstStart;
}
void setup()
{
Serial.begin(115200);
Log = &Serial;
Log->print(F("Nuki Hub version ")); Log->println(NUKI_HUB_VERSION);
bool firstStart = initPreferences();
initializeRestartReason();
uint32_t devIdOpener = preferences->getUInt(preference_device_id_opener);
deviceIdLock = new NukiDeviceId(preferences, preference_device_id_lock);
deviceIdOpener = new NukiDeviceId(preferences, preference_device_id_opener);
if(deviceIdLock->get() != 0 && devIdOpener == 0)
{
deviceIdOpener->assignId(deviceIdLock->get());
}
CharBuffer::initialize();
if(preferences->getInt(preference_restart_timer) != 0)
{
preferences->remove(preference_restart_timer);
}
gpio = new Gpio(preferences);
String gpioDesc;
gpio->getConfigurationText(gpioDesc, gpio->pinConfiguration(), "\n\r");
Serial.print(gpioDesc.c_str());
lockEnabled = preferences->getBool(preference_lock_enabled);
openerEnabled = preferences->getBool(preference_opener_enabled);
const String mqttLockPath = preferences->getString(preference_mqtt_lock_path);
network = new Network(preferences, gpio, mqttLockPath, CharBuffer::get(), CHAR_BUFFER_SIZE);
network->initialize();
networkLock = new NetworkLock(network, preferences, CharBuffer::get(), CHAR_BUFFER_SIZE);
networkLock->initialize();
if(openerEnabled)
{
networkOpener = new NetworkOpener(network, preferences, CharBuffer::get(), CHAR_BUFFER_SIZE);
networkOpener->initialize();
}
initEthServer(network->networkDeviceType());
bleScanner = new BleScanner::Scanner();
bleScanner->initialize("NukiHub");
bleScanner->setScanDuration(10);
Log->println(lockEnabled ? F("Nuki Lock enabled") : F("Nuki Lock disabled"));
if(lockEnabled)
{
nuki = new NukiWrapper("NukiHub", deviceIdLock, bleScanner, networkLock, gpio, preferences);
nuki->initialize(firstStart);
}
Log->println(openerEnabled ? F("Nuki Opener enabled") : F("Nuki Opener disabled"));
if(openerEnabled)
{
nukiOpener = new NukiOpenerWrapper("NukiHub", deviceIdOpener, bleScanner, networkOpener, gpio, preferences);
nukiOpener->initialize();
}
webCfgServer = new WebCfgServer(nuki, nukiOpener, network, gpio, ethServer, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi);
webCfgServer->initialize();
presenceDetection = new PresenceDetection(preferences, bleScanner, network, CharBuffer::get(), CHAR_BUFFER_SIZE);
presenceDetection->initialize();
setupTasks();
}
void loop()
{
delay(60000);
}

View File

@@ -0,0 +1,75 @@
/*
Copyright (c) 2022 Bert Melis. All rights reserved.
This work is licensed under the terms of the MIT license.
For a copy, see <https://opensource.org/licenses/MIT> or
the LICENSE file.
*/
#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
#include "ClientSyncW5500.h"
#include <lwip/sockets.h> // socket options
namespace espMqttClientInternals {
ClientSyncW5500::ClientSyncW5500()
: client() {
// empty
}
bool ClientSyncW5500::connect(IPAddress ip, uint16_t port) {
bool ret = client.connect(ip, port); // implicit conversion of return code int --> bool
if (ret) {
#if defined(ARDUINO_ARCH_ESP8266)
client.setNoDelay(true);
#elif defined(ARDUINO_ARCH_ESP32)
// Set TCP option directly to bypass lack of working setNoDelay for WiFiClientSecure (for consistency also here)
int val = true;
// TODO
// client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
#endif
}
return ret;
}
bool ClientSyncW5500::connect(const char* host, uint16_t port) {
bool ret = client.connect(host, port); // implicit conversion of return code int --> bool
if (ret) {
#if defined(ARDUINO_ARCH_ESP8266)
client.setNoDelay(true);
#elif defined(ARDUINO_ARCH_ESP32)
// Set TCP option directly to bypass lack of working setNoDelay for WiFiClientSecure (for consistency also here)
int val = true;
// TODO
// client.setSocketOption(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
#endif
}
return ret;
}
size_t ClientSyncW5500::write(const uint8_t* buf, size_t size) {
return client.write(buf, size);
}
int ClientSyncW5500::read(uint8_t* buf, size_t size) {
return client.read(buf, size);
}
void ClientSyncW5500::stop() {
client.stop();
}
bool ClientSyncW5500::connected() {
return client.connected();
}
bool ClientSyncW5500::disconnected() {
return !client.connected();
}
} // namespace espMqttClientInternals
#endif

View File

@@ -0,0 +1,25 @@
#pragma once
#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
#include "Transport/Transport.h"
#include "EthernetClient.h"
namespace espMqttClientInternals {
class ClientSyncW5500 : public Transport {
public:
ClientSyncW5500();
bool connect(IPAddress ip, uint16_t port) override;
bool connect(const char* host, uint16_t port) override;
size_t write(const uint8_t* buf, size_t size) override;
int read(uint8_t* buf, size_t size) override;
void stop() override;
bool connected() override;
bool disconnected() override;
EthernetClient client;
};
} // namespace espMqttClientInternals
#endif

View File

@@ -0,0 +1,150 @@
//#define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT
//#define ETH_PHY_POWER 12
#include <WiFi.h>
#include <ETH.h>
#include "EthLan8720Device.h"
#include "../PreferencesKeys.h"
#include "../Logger.h"
#include "../MqttTopics.h"
#include "espMqttClient.h"
#include "../RestartReason.h"
EthLan8720Device::EthLan8720Device(const String& hostname, Preferences* preferences, const IPConfiguration* ipConfiguration, const std::string& deviceName, uint8_t phy_addr, int power, int mdc, int mdio, eth_phy_type_t ethtype, eth_clock_mode_t clock_mode, bool use_mac_from_efuse)
: NetworkDevice(hostname, ipConfiguration),
_deviceName(deviceName),
_phy_addr(phy_addr),
_power(power),
_mdc(mdc),
_mdio(mdio),
_type(ethtype),
_clock_mode(clock_mode),
_use_mac_from_efuse(use_mac_from_efuse)
{
_restartOnDisconnect = preferences->getBool(preference_restart_on_disconnect);
size_t caLength = preferences->getString(preference_mqtt_ca, _ca, TLS_CA_MAX_SIZE);
size_t crtLength = preferences->getString(preference_mqtt_crt, _cert, TLS_CERT_MAX_SIZE);
size_t keyLength = preferences->getString(preference_mqtt_key, _key, TLS_KEY_MAX_SIZE);
_useEncryption = caLength > 1; // length is 1 when empty
if(_useEncryption)
{
Log->println(F("MQTT over TLS."));
Log->println(_ca);
_mqttClientSecure = new espMqttClientSecure(espMqttClientTypes::UseInternalTask::NO);
_mqttClientSecure->setCACert(_ca);
if(crtLength > 1 && keyLength > 1) // length is 1 when empty
{
Log->println(F("MQTT with client certificate."));
Log->println(_cert);
Log->println(_key);
_mqttClientSecure->setCertificate(_cert);
_mqttClientSecure->setPrivateKey(_key);
}
} else
{
Log->println(F("MQTT without TLS."));
_mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO);
}
if(preferences->getBool(preference_mqtt_log_enabled))
{
_path = new char[200];
memset(_path, 0, sizeof(_path));
String pathStr = preferences->getString(preference_mqtt_lock_path);
pathStr.concat(mqtt_topic_log);
strcpy(_path, pathStr.c_str());
Log = new MqttLogger(*getMqttClient(), _path, MqttLoggerMode::MqttAndSerial);
}
}
const String EthLan8720Device::deviceName() const
{
return _deviceName.c_str();
}
void EthLan8720Device::initialize()
{
delay(250);
WiFi.setHostname(_hostname.c_str());
_hardwareInitialized = ETH.begin(_phy_addr, _power, _mdc, _mdio, _type, _clock_mode, _use_mac_from_efuse);
ETH.setHostname(_hostname.c_str());
if(!_ipConfiguration->dhcpEnabled())
{
ETH.config(_ipConfiguration->ipAddress(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet(), _ipConfiguration->dnsServer());
}
if(_restartOnDisconnect)
{
WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info)
{
if(event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED)
{
onDisconnected();
}
});
}
}
void EthLan8720Device::reconfigure()
{
delay(200);
restartEsp(RestartReason::ReconfigureLAN8720);
}
bool EthLan8720Device::supportsEncryption()
{
return true;
}
bool EthLan8720Device::isConnected()
{
bool connected = ETH.linkUp();
if(_lastConnected == false && connected == true)
{
Serial.print(F("Ethernet connected. IP address: "));
Serial.println(ETH.localIP().toString());
}
_lastConnected = connected;
return connected;
}
ReconnectStatus EthLan8720Device::reconnect()
{
if(!_hardwareInitialized)
{
return ReconnectStatus::CriticalFailure;
}
delay(200);
return isConnected() ? ReconnectStatus::Success : ReconnectStatus::Failure;
}
void EthLan8720Device::onDisconnected()
{
if(millis() > 60000)
{
restartEsp(RestartReason::RestartOnDisconnectWatchdog);
}
}
int8_t EthLan8720Device::signalStrength()
{
return -1;
}
String EthLan8720Device::localIP()
{
return ETH.localIP().toString();
}
String EthLan8720Device::BSSIDstr()
{
return "";
}

View File

@@ -0,0 +1,61 @@
#pragma once
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <Preferences.h>
#include "NetworkDevice.h"
#include "espMqttClient.h"
#include <ETH.h>
class EthLan8720Device : public NetworkDevice
{
public:
EthLan8720Device(const String& hostname,
Preferences* preferences,
const IPConfiguration* ipConfiguration,
const std::string& deviceName,
uint8_t phy_addr = ETH_PHY_ADDR,
int power = ETH_PHY_POWER,
int mdc = ETH_PHY_MDC,
int mdio = ETH_PHY_MDIO,
eth_phy_type_t ethtype = ETH_PHY_TYPE,
eth_clock_mode_t clock_mode = ETH_CLK_MODE,
bool use_mac_from_efuse = false);
const String deviceName() const override;
virtual void initialize();
virtual void reconfigure();
virtual ReconnectStatus reconnect();
bool supportsEncryption() override;
virtual bool isConnected();
int8_t signalStrength() override;
String localIP() override;
String BSSIDstr() override;
private:
void onDisconnected();
bool _restartOnDisconnect = false;
bool _startAp = false;
char* _path;
bool _hardwareInitialized = false;
bool _lastConnected = false;
const std::string _deviceName;
uint8_t _phy_addr;
int _power;
int _mdc;
int _mdio;
eth_phy_type_t _type;
eth_clock_mode_t _clock_mode;
bool _use_mac_from_efuse;
char _ca[TLS_CA_MAX_SIZE] = {0};
char _cert[TLS_CERT_MAX_SIZE] = {0};
char _key[TLS_KEY_MAX_SIZE] = {0};
};

View File

@@ -0,0 +1,56 @@
#include "IPConfiguration.h"
#include "../PreferencesKeys.h"
#include "../Logger.h"
IPConfiguration::IPConfiguration(Preferences *preferences)
: _preferences(preferences)
{
if(_preferences->getString(preference_ip_address).length() <= 0)
{
Log->println("IP address empty, falling back to DHCP.");
_preferences->putBool(preference_ip_dhcp_enabled, true);
}
_ipAddress.fromString(_preferences->getString(preference_ip_address));
_subnet.fromString(_preferences->getString(preference_ip_subnet));
_gateway.fromString(_preferences->getString(preference_ip_gateway));
_dnsServer.fromString(_preferences->getString(preference_ip_dns_server));
Log->print(F("IP configuration: "));
if(dhcpEnabled())
{
Log->println(F("DHCP"));
}
else
{
Log->print(F("IP address: ")); Log->print(ipAddress());
Log->print(F(", Subnet: ")); Log->print(subnet());
Log->print(F(", Gateway: ")); Log->print(defaultGateway());
Log->print(F(", DNS: ")); Log->println(dnsServer());
}
}
bool IPConfiguration::dhcpEnabled() const
{
return _preferences->getBool(preference_ip_dhcp_enabled);
}
const IPAddress IPConfiguration::ipAddress() const
{
return _ipAddress;
}
const IPAddress IPConfiguration::subnet() const
{
return _subnet;
}
const IPAddress IPConfiguration::defaultGateway() const
{
return _gateway;
}
const IPAddress IPConfiguration::dnsServer() const
{
return _dnsServer;
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <Preferences.h>
class IPConfiguration
{
public:
explicit IPConfiguration(Preferences* preferences);
bool dhcpEnabled() const;
const IPAddress ipAddress() const;
const IPAddress subnet() const;
const IPAddress defaultGateway() const;
const IPAddress dnsServer() const;
private:
Preferences* _preferences = nullptr;
IPAddress _ipAddress;
IPAddress _subnet;
IPAddress _gateway;
IPAddress _dnsServer;
};

View File

@@ -0,0 +1,161 @@
#include <Arduino.h>
#include "NetworkDevice.h"
#include "../Logger.h"
void NetworkDevice::printError()
{
Log->print(F("Free Heap: "));
Log->println(ESP.getFreeHeap());
}
void NetworkDevice::update()
{
if (_mqttEnabled)
{
getMqttClient()->loop();
}
}
void NetworkDevice::mqttSetClientId(const char *clientId)
{
if (_useEncryption)
{
_mqttClientSecure->setClientId(clientId);
}
else
{
_mqttClient->setClientId(clientId);
}
}
void NetworkDevice::mqttSetCleanSession(bool cleanSession)
{
if (_useEncryption)
{
_mqttClientSecure->setCleanSession(cleanSession);
}
else
{
_mqttClient->setCleanSession(cleanSession);
}
}
uint16_t NetworkDevice::mqttPublish(const char *topic, uint8_t qos, bool retain, const char *payload)
{
return getMqttClient()->publish(topic, qos, retain, payload);
}
uint16_t NetworkDevice::mqttPublish(const char *topic, uint8_t qos, bool retain, const uint8_t *payload, size_t length)
{
return getMqttClient()->publish(topic, qos, retain, payload, length);
}
bool NetworkDevice::mqttConnected() const
{
return getMqttClient()->connected();
}
void NetworkDevice::mqttSetServer(const char *host, uint16_t port)
{
if (_useEncryption)
{
_mqttClientSecure->setServer(host, port);
}
else
{
_mqttClient->setServer(host, port);
}
}
bool NetworkDevice::mqttConnect()
{
return getMqttClient()->connect();
}
bool NetworkDevice::mqttDisconnect(bool force)
{
return getMqttClient()->disconnect(force);
}
void NetworkDevice::setWill(const char *topic, uint8_t qos, bool retain, const char *payload)
{
if (_useEncryption)
{
_mqttClientSecure->setWill(topic, qos, retain, payload);
}
else
{
_mqttClient->setWill(topic, qos, retain, payload);
}
}
void NetworkDevice::mqttSetCredentials(const char *username, const char *password)
{
if (_useEncryption)
{
_mqttClientSecure->setCredentials(username, password);
}
else
{
_mqttClient->setCredentials(username, password);
}
}
void NetworkDevice::mqttOnMessage(espMqttClientTypes::OnMessageCallback callback)
{
if (_useEncryption)
{
_mqttClientSecure->onMessage(callback);
}
else
{
_mqttClient->onMessage(callback);
}
}
void NetworkDevice::mqttOnConnect(espMqttClientTypes::OnConnectCallback callback)
{
if(_useEncryption)
{
_mqttClientSecure->onConnect(callback);
}
else
{
_mqttClient->onConnect(callback);
}
}
void NetworkDevice::mqttOnDisconnect(espMqttClientTypes::OnDisconnectCallback callback)
{
if (_useEncryption)
{
_mqttClientSecure->onDisconnect(callback);
}
else
{
_mqttClient->onDisconnect(callback);
}
}
uint16_t NetworkDevice::mqttSubscribe(const char *topic, uint8_t qos)
{
return getMqttClient()->subscribe(topic, qos);
}
void NetworkDevice::disableMqtt()
{
getMqttClient()->disconnect();
_mqttEnabled = false;
}
MqttClient *NetworkDevice::getMqttClient() const
{
if (_useEncryption)
{
return _mqttClientSecure;
}
else
{
return _mqttClient;
}
}

View File

@@ -0,0 +1,66 @@
#pragma once
#include "espMqttClient.h"
#include "MqttClientSetup.h"
#include "IPConfiguration.h"
enum class ReconnectStatus
{
Failure = 0,
Success = 1,
CriticalFailure = 2
};
class NetworkDevice
{
public:
explicit NetworkDevice(const String& hostname, const IPConfiguration* ipConfiguration)
: _hostname(hostname),
_ipConfiguration(ipConfiguration)
{}
virtual const String deviceName() const = 0;
virtual void initialize() = 0;
virtual ReconnectStatus reconnect() = 0;
virtual void reconfigure() = 0;
virtual void printError();
virtual bool supportsEncryption() = 0;
virtual void update();
virtual bool isConnected() = 0;
virtual int8_t signalStrength() = 0;
virtual String localIP() = 0;
virtual String BSSIDstr() = 0;
virtual void mqttSetClientId(const char* clientId);
virtual void mqttSetCleanSession(bool cleanSession);
virtual uint16_t mqttPublish(const char* topic, uint8_t qos, bool retain, const char* payload);
virtual uint16_t mqttPublish(const char* topic, uint8_t qos, bool retain, const uint8_t* payload, size_t length);
virtual bool mqttConnected() const;
virtual void mqttSetServer(const char* host, uint16_t port);
virtual bool mqttConnect();
virtual bool mqttDisconnect(bool force);
virtual void setWill(const char* topic, uint8_t qos, bool retain, const char* payload);
virtual void mqttSetCredentials(const char* username, const char* password);
virtual void mqttOnMessage(espMqttClientTypes::OnMessageCallback callback);
virtual void mqttOnConnect(espMqttClientTypes::OnConnectCallback callback);
virtual void mqttOnDisconnect(espMqttClientTypes::OnDisconnectCallback callback);
virtual void disableMqtt();
virtual uint16_t mqttSubscribe(const char* topic, uint8_t qos);
protected:
espMqttClient *_mqttClient = nullptr;
espMqttClientSecure *_mqttClientSecure = nullptr;
bool _useEncryption = false;
bool _mqttEnabled = true;
const String _hostname;
const IPConfiguration* _ipConfiguration = nullptr;
MqttClient *getMqttClient() const;
};

View File

@@ -0,0 +1,232 @@
#include <Arduino.h>
#include <WiFi.h>
#include "W5500Device.h"
#include "../PreferencesKeys.h"
#include "../Logger.h"
#include "../MqttTopics.h"
W5500Device::W5500Device(const String &hostname, Preferences* preferences, const IPConfiguration* ipConfiguration, int variant)
: NetworkDevice(hostname, ipConfiguration),
_preferences(preferences),
_variant((W5500Variant)variant)
{
initializeMacAddress(_mac);
Log->print("MAC Adress: ");
for(int i=0; i < 6; i++)
{
if(_mac[i] < 10)
{
Log->print(F("0"));
}
Log->print(_mac[i], 16);
if(i < 5)
{
Log->print(F(":"));
}
}
Log->println();
_mqttClient = new espMqttClientW5500();
}
W5500Device::~W5500Device()
{}
const String W5500Device::deviceName() const
{
return "Wiznet W5500";
}
void W5500Device::initialize()
{
WiFi.mode(WIFI_OFF);
resetDevice();
switch(_variant)
{
case W5500Variant::M5StackAtomPoe:
_resetPin = -1;
Ethernet.init(19, 22, 23, 33);
break;
default:
_resetPin = -1;
Ethernet.init(5);
break;
}
if(_preferences->getBool(preference_mqtt_log_enabled))
{
String pathStr = _preferences->getString(preference_mqtt_lock_path);
pathStr.concat(mqtt_topic_log);
_path = new char[pathStr.length() + 1];
memset(_path, 0, sizeof(_path));
strcpy(_path, pathStr.c_str());
Log = new MqttLogger(*getMqttClient(), _path, MqttLoggerMode::MqttAndSerial);
}
reconnect();
}
ReconnectStatus W5500Device::reconnect()
{
_hasDHCPAddress = false;
// start the Ethernet connection:
Log->println(F("Initialize Ethernet with DHCP:"));
int dhcpRetryCnt = 0;
bool hardwareFound = false;
while(dhcpRetryCnt < 3)
{
Log->print(F("DHCP connect try #"));
Log->print(dhcpRetryCnt);
Log->println();
dhcpRetryCnt++;
if (Ethernet.begin(_mac, 1000, 1000) == 0)
{
Log->println(F("Failed to configure Ethernet using DHCP"));
// Check for Ethernet hardware present
if (Ethernet.hardwareStatus() == EthernetNoHardware)
{
Log->println(F("Ethernet module not found"));
continue;
}
if (Ethernet.linkStatus() == LinkOFF)
{
Log->println(F("Ethernet cable is not connected."));
}
hardwareFound = true;
IPAddress ip;
ip.fromString("192.168.4.1");
IPAddress subnet;
subnet.fromString("255.255.255.0");
// try to congifure using IP address instead of DHCP:
Ethernet.begin(_mac, ip);
Ethernet.setSubnetMask(subnet);
delay(1000);
}
else
{
hardwareFound = true;
_hasDHCPAddress = true;
dhcpRetryCnt = 1000;
if(_ipConfiguration->dhcpEnabled())
{
Log->print(F(" DHCP assigned IP "));
Log->println(Ethernet.localIP());
}
}
if(!_ipConfiguration->dhcpEnabled())
{
Ethernet.setLocalIP(_ipConfiguration->ipAddress());
Ethernet.setSubnetMask(_ipConfiguration->subnet());
Ethernet.setGatewayIP(_ipConfiguration->defaultGateway());
Ethernet.setDnsServerIP(_ipConfiguration->dnsServer());
}
}
if(!hardwareFound)
{
return ReconnectStatus::CriticalFailure;
}
return _hasDHCPAddress ? ReconnectStatus::Success : ReconnectStatus::Failure;
}
void W5500Device::reconfigure()
{
Log->println(F("Reconfigure W5500 not implemented."));
}
void W5500Device::resetDevice()
{
if(_resetPin == -1) return;
Log->println(F("Resetting network hardware."));
pinMode(_resetPin, OUTPUT);
digitalWrite(_resetPin, HIGH);
delay(50);
digitalWrite(_resetPin, LOW);
delay(50);
digitalWrite(_resetPin, HIGH);
delay(50);
}
bool W5500Device::supportsEncryption()
{
return false;
}
bool W5500Device::isConnected()
{
bool connected = (Ethernet.linkStatus() == EthernetLinkStatus::LinkON && _maintainResult == 0 && _hasDHCPAddress);
if(_lastConnected == false && connected == true)
{
Serial.print(F("Ethernet connected. IP address: "));
Serial.println(Ethernet.localIP().toString());
}
_lastConnected = connected;
return connected;
}
void W5500Device::initializeMacAddress(byte *mac)
{
memset(mac, 0, 6);
mac[0] = 0x00; // wiznet prefix
mac[1] = 0x08; // wiznet prefix
mac[2] = 0xDC; // wiznet prefix
if(_preferences->getBool(preference_has_mac_saved))
{
mac[3] = _preferences->getChar(preference_has_mac_byte_0);
mac[4] = _preferences->getChar(preference_has_mac_byte_1);
mac[5] = _preferences->getChar(preference_has_mac_byte_2);
}
else
{
mac[3] = random(0,255);
mac[4] = random(0,255);
mac[5] = random(0,255);
_preferences->putChar(preference_has_mac_byte_0, mac[3]);
_preferences->putChar(preference_has_mac_byte_1, mac[4]);
_preferences->putChar(preference_has_mac_byte_2, mac[5]);
_preferences->putBool(preference_has_mac_saved, true);
}
}
void W5500Device::update()
{
_maintainResult = Ethernet.maintain();
NetworkDevice::update();
}
int8_t W5500Device::signalStrength()
{
return 127;
}
String W5500Device::localIP()
{
return Ethernet.localIP().toString();
}
String W5500Device::BSSIDstr()
{
return "";
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include "NetworkDevice.h"
#include "espMqttClient.h"
#include "espMqttClientW5500.h"
#include <Ethernet.h>
#include <Preferences.h>
enum class W5500Variant
{
Generic = 2,
M5StackAtomPoe = 3
};
class W5500Device : public NetworkDevice
{
public:
explicit W5500Device(const String& hostname, Preferences* _preferences, const IPConfiguration* ipConfiguration, int variant);
~W5500Device();
const String deviceName() const override;
virtual void initialize();
virtual ReconnectStatus reconnect();
virtual void reconfigure();
bool supportsEncryption() override;
virtual void update() override;
virtual bool isConnected();
int8_t signalStrength() override;
String localIP() override;
String BSSIDstr() override;
private:
void resetDevice();
void initializeMacAddress(byte* mac);
Preferences* _preferences = nullptr;
int _maintainResult = 0;
int _resetPin = -1;
bool _hasDHCPAddress = false;
char* _path;
W5500Variant _variant;
bool _lastConnected = false;
byte _mac[6];
};

View File

@@ -0,0 +1,171 @@
#include <WiFi.h>
#include "WifiDevice.h"
#include "../PreferencesKeys.h"
#include "../Logger.h"
#include "../MqttTopics.h"
#include "espMqttClient.h"
#include "../RestartReason.h"
RTC_NOINIT_ATTR char WiFiDevice_reconfdetect[17];
WifiDevice::WifiDevice(const String& hostname, Preferences* preferences, const IPConfiguration* ipConfiguration)
: NetworkDevice(hostname, ipConfiguration),
_preferences(preferences),
_wm(preferences->getString(preference_cred_user).c_str(), preferences->getString(preference_cred_password).c_str())
{
_startAp = strcmp(WiFiDevice_reconfdetect, "reconfigure_wifi") == 0;
_restartOnDisconnect = preferences->getBool(preference_restart_on_disconnect);
size_t caLength = preferences->getString(preference_mqtt_ca, _ca, TLS_CA_MAX_SIZE);
size_t crtLength = preferences->getString(preference_mqtt_crt, _cert, TLS_CERT_MAX_SIZE);
size_t keyLength = preferences->getString(preference_mqtt_key, _key, TLS_KEY_MAX_SIZE);
_useEncryption = caLength > 1; // length is 1 when empty
if(_useEncryption)
{
Log->println(F("MQTT over TLS."));
Log->println(_ca);
_mqttClientSecure = new espMqttClientSecure(espMqttClientTypes::UseInternalTask::NO);
_mqttClientSecure->setCACert(_ca);
if(crtLength > 1 && keyLength > 1) // length is 1 when empty
{
Log->println(F("MQTT with client certificate."));
Log->println(_cert);
Log->println(_key);
_mqttClientSecure->setCertificate(_cert);
_mqttClientSecure->setPrivateKey(_key);
}
} else
{
Log->println(F("MQTT without TLS."));
_mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO);
}
if(preferences->getBool(preference_mqtt_log_enabled))
{
_path = new char[200];
memset(_path, 0, sizeof(_path));
String pathStr = preferences->getString(preference_mqtt_lock_path);
pathStr.concat(mqtt_topic_log);
strcpy(_path, pathStr.c_str());
Log = new MqttLogger(*getMqttClient(), _path, MqttLoggerMode::MqttAndSerial);
}
}
const String WifiDevice::deviceName() const
{
return "Built-in Wi-Fi";
}
void WifiDevice::initialize()
{
std::vector<const char *> wm_menu;
wm_menu.push_back("wifi");
wm_menu.push_back("exit");
_wm.setEnableConfigPortal(_startAp || !_preferences->getBool(preference_network_wifi_fallback_disabled));
// reduced tieout if ESP is set to restart on disconnect
_wm.setFindBestRSSI(_preferences->getBool(preference_find_best_rssi));
_wm.setConfigPortalTimeout(_restartOnDisconnect ? 60 * 3 : 60 * 30);
_wm.setShowInfoUpdate(false);
_wm.setMenu(wm_menu);
_wm.setHostname(_hostname);
if(!_ipConfiguration->dhcpEnabled())
{
_wm.setSTAStaticIPConfig(_ipConfiguration->ipAddress(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet(), _ipConfiguration->dnsServer());
}
_wm.setAPCallback(clearRtcInitVar);
bool res = false;
if(_startAp)
{
Log->println(F("Opening Wi-Fi configuration portal."));
res = _wm.startConfigPortal();
}
else
{
res = _wm.autoConnect(); // password protected ap
}
if(!res)
{
esp_wifi_disconnect ();
esp_wifi_stop ();
esp_wifi_deinit ();
Log->println(F("Failed to connect. Wait for ESP restart."));
delay(1000);
restartEsp(RestartReason::WifiInitFailed);
}
else {
Log->print(F("Wi-Fi connected: "));
Log->println(WiFi.localIP().toString());
}
if(_restartOnDisconnect)
{
WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info)
{
if(event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED)
{
onDisconnected();
}
});
}
}
void WifiDevice::reconfigure()
{
strcpy(WiFiDevice_reconfdetect, "reconfigure_wifi");
delay(200);
restartEsp(RestartReason::ReconfigureWifi);
}
bool WifiDevice::supportsEncryption()
{
return true;
}
bool WifiDevice::isConnected()
{
return WiFi.isConnected();
}
ReconnectStatus WifiDevice::reconnect()
{
delay(3000);
return isConnected() ? ReconnectStatus::Success : ReconnectStatus::Failure;
}
void WifiDevice::onDisconnected()
{
if(millis() > 60000)
{
restartEsp(RestartReason::RestartOnDisconnectWatchdog);
}
}
int8_t WifiDevice::signalStrength()
{
return WiFi.RSSI();
}
String WifiDevice::localIP()
{
return WiFi.localIP().toString();
}
String WifiDevice::BSSIDstr()
{
return WiFi.BSSIDstr();
}
void WifiDevice::clearRtcInitVar(WiFiManager *)
{
memset(WiFiDevice_reconfdetect, 0, sizeof WiFiDevice_reconfdetect);
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <Preferences.h>
#include "NetworkDevice.h"
#include "WiFiManager.h"
#include "espMqttClient.h"
#include "IPConfiguration.h"
class WifiDevice : public NetworkDevice
{
public:
WifiDevice(const String& hostname, Preferences* preferences, const IPConfiguration* ipConfiguration);
const String deviceName() const override;
virtual void initialize();
virtual void reconfigure();
virtual ReconnectStatus reconnect();
bool supportsEncryption() override;
virtual bool isConnected();
int8_t signalStrength() override;
String localIP() override;
String BSSIDstr() override;
private:
static void clearRtcInitVar(WiFiManager*);
void onDisconnected();
WiFiManager _wm;
Preferences* _preferences = nullptr;
bool _restartOnDisconnect = false;
bool _startAp = false;
char* _path;
char _ca[TLS_CA_MAX_SIZE] = {0};
char _cert[TLS_CERT_MAX_SIZE] = {0};
char _key[TLS_KEY_MAX_SIZE] = {0};
};

View File

@@ -0,0 +1,13 @@
#include "espMqttClientW5500.h"
espMqttClientW5500::espMqttClientW5500()
: espMqttClient(espMqttClientTypes::UseInternalTask::NO),
_client()
{
_transport = &_client;
}
void espMqttClientW5500::update()
{
loop();
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include "espMqttClient.h"
#include "ClientSyncW5500.h"
class espMqttClientW5500 : public espMqttClient {
public:
#if defined(ARDUINO_ARCH_ESP32)
explicit espMqttClientW5500();
#else
espMqttClient();
#endif
void update();
protected:
#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
espMqttClientInternals::ClientSyncW5500 _client;
#elif defined(__linux__)
espMqttClientInternals::ClientPosix _client;
#endif
};