Skip to content

Commit

Permalink
Add Battery reporting from a Smart Lock (#153)
Browse files Browse the repository at this point in the history
### TL;DR
Added SmartLock battery level reporting functionality via MQTT and HomeKit integration.

### What changed?
- Added battery level reporting configuration options in the web UI
- Implemented battery level threshold settings (0-100%)
- Created new MQTT topic for battery level commands
- Added HomeKit battery service with low status and level characteristics
- Introduced new CLI commands for debugging purposes ('N' and 'B')

### How to test?
1. Enable SmartLock battery reporting in the Misc settings
2. Set desired battery low threshold percentage
3. Configure MQTT battery level command topic
4. Send battery level updates via MQTT or CLI commands:
   - Use 'N 0/1' to toggle battery low status
   - Use 'B [level]' to set battery percentage
5. Verify battery status appears in HomeKit
6. Confirm low battery warnings trigger when level drops below threshold

### Why make this change?
Enables monitoring of physical lock battery levels through HomeKit and MQTT, allowing users to receive notifications when batteries need replacement and maintain better awareness of their lock's power status.
  • Loading branch information
rednblkx authored Nov 17, 2024
2 parents aec4631 + 4fea224 commit df3bc49
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 7 deletions.
18 changes: 14 additions & 4 deletions data/routes/misc.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,16 @@ <h3 style="margin: 0;width: fit-content;padding: 0.5rem;cursor: pointer;" onclic
<span style="height: 1px;border-top: 1px #424242 solid;display: block;margin: 0;padding: 0;"></span>
</div>
<div class="custom-tabs-selected-body" data-custom-tabs-body="0">
<div style="display: flex;flex-direction: column;">
<div style="display: flex;gap: 8px;">
<label for="device-name">Device Name</label>
<input type="text" name="device-name" id="device-name" placeholder="HK" required value="%DEVICENAME%"
style="width: fit-content;" />
</div>

<div style="display: flex;flex-direction: column;">
<div style="display: flex;gap: 8px;">
<label for="hk-setupcode">Setup Code</label>
<input type="number" name="hk-setupcode" id="hk-setupcode" placeholder="46637726" required
value="%HKSETUPCODE%" style="width: fit-content;" />
</div>

<div style="display: flex;gap: 8px;">
<label for="hk-always-lock">Always Lock on HomeKey</label>
<select name="hk-always-lock" id="hk-always-lock">
Expand All @@ -40,6 +38,17 @@ <h3 style="margin: 0;width: fit-content;padding: 0.5rem;cursor: pointer;" onclic
<option value="1">Enabled</option>
</select>
</div>
<div style="display: flex;gap: 8px;">
<label for="prox-bat-enable">SmartLock battery reporting</label>
<select name="prox-bat-enable" id="prox-bat-enable">
<option value="0">Disabled</option>
<option value="1">Enabled</option>
</select>
</div>
<div style="display: flex;gap: 8px;">
<label for="btr-low-threshold">Battery low status Threshold</label>
<input type="number" name="btr-low-threshold" id="btr-low-threshold" placeholder="10" min="0" max="100" value="%BTRLOWTHRESHOLD%" style="width: 4rem;" />
</div>
<fieldset>
<legend>HomeKey Card Finish:</legend>
<div style="display: flex;justify-content: space-evenly;margin-bottom: 0;padding-bottom: 0;">
Expand Down Expand Up @@ -170,6 +179,7 @@ <h3 class="webui-tabs-selected-tab" style="margin: 0;width: fit-content;padding:
document.getElementById("hk-always-unlock").selectedIndex = "%ALWAYSUNLOCK%";
document.getElementById("hk-always-lock").selectedIndex = "%ALWAYSLOCK%";
document.getElementById("web-auth-enable").selectedIndex = "%WEBENABLE%";
document.getElementById("prox-bat-enable").selectedIndex = "%PROXBATENABLE%";
let form = document.getElementById("misc-config");
async function handleForm(event) {
event.preventDefault();
Expand Down
4 changes: 4 additions & 0 deletions data/routes/mqtt.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ <h3 style="text-align: center;margin-top: 0;">MQTT Topics</h3>
<label for="mqtt-tstatecmd">Lock Target State Cmd Topic</label>
<input type="text" name="mqtt-tstatecmd" id="mqtt-tstatecmd" placeholder="topic/set_target_state" required value="%TSTATECMD%">
</div>
<div style="display: flex; flex-direction: column;">
<label for="mqtt-btrprox-cmd-topic">SmartLock battery level Cmd Topic</label>
<input type="text" name="mqtt-btrprox-cmd-topic" id="mqtt-btrprox-cmd-topic" placeholder="topic/set_battery_level" required value="%BTRLEVELCMD%">
</div>
</div>
</div>
<div class="mqtt-topics-hidden-body" data-mqtt-topics-body="1">
Expand Down
1 change: 1 addition & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ enum class gpioMomentaryStateStatus : uint8_t
#define MQTT_SET_TARGET_STATE_TOPIC "homekit/set_target_state" // MQTT Control Topic for the HomeKit lock target state
#define MQTT_SET_CURRENT_STATE_TOPIC "homekit/set_current_state" // MQTT Control Topic for the HomeKit lock current state
#define MQTT_STATE_TOPIC "homekit/state" // MQTT Topic for publishing the HomeKit lock target state
#define MQTT_PROX_BAT_TOPIC "homekit/set_battery_lvl" // MQTT Topic for publishing the HomeKit lock target state

// Miscellaneous
#define HOMEKEY_COLOR TAN
Expand Down
64 changes: 61 additions & 3 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ namespace espConfig
lockTStateCmd.append(id).append("/" MQTT_SET_TARGET_STATE_TOPIC);
lockCustomStateTopic.append(id).append("/" MQTT_CUSTOM_STATE_TOPIC);
lockCustomStateCmd.append(id).append("/" MQTT_CUSTOM_STATE_CTRL_TOPIC);
btrLvlCmdTopic.append(id).append("/" MQTT_PROX_BAT_TOPIC);
}
/* MQTT Broker */
std::string mqttBroker = MQTT_HOST;
Expand All @@ -85,6 +86,7 @@ namespace espConfig
std::string lockStateCmd;
std::string lockCStateCmd;
std::string lockTStateCmd;
std::string btrLvlCmdTopic;
/* MQTT Custom State */
std::string lockCustomStateTopic;
std::string lockCustomStateCmd;
Expand All @@ -94,7 +96,7 @@ namespace espConfig
bool nfcTagNoPublish = false;
std::map<std::string, int> customLockStates = { {"C_LOCKED", C_LOCKED}, {"C_UNLOCKING", C_UNLOCKING}, {"C_UNLOCKED", C_UNLOCKED}, {"C_LOCKING", C_LOCKING}, {"C_JAMMED", C_JAMMED}, {"C_UNKNOWN", C_UNKNOWN} };
std::map<std::string, int> customLockActions = { {"UNLOCK", UNLOCK}, {"LOCK", LOCK} };
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(espConfig::mqttConfig_t, mqttBroker, mqttPort, mqttUsername, mqttPassword, mqttClientId, lwtTopic, hkTopic, lockStateTopic, lockStateCmd, lockCStateCmd, lockTStateCmd, lockCustomStateTopic, lockCustomStateCmd, lockEnableCustomState, hassMqttDiscoveryEnabled, customLockStates, customLockActions, nfcTagNoPublish)
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(espConfig::mqttConfig_t, mqttBroker, mqttPort, mqttUsername, mqttPassword, mqttClientId, lwtTopic, hkTopic, lockStateTopic, lockStateCmd, lockCStateCmd, lockTStateCmd, lockCustomStateTopic, lockCustomStateCmd, lockEnableCustomState, hassMqttDiscoveryEnabled, customLockStates, customLockActions, nfcTagNoPublish, btrLvlCmdTopic)
} mqttData;

struct misc_config_t
Expand Down Expand Up @@ -135,13 +137,17 @@ namespace espConfig
std::string webUsername = WEB_AUTH_USERNAME;
std::string webPassword = WEB_AUTH_PASSWORD;
std::array<uint8_t, 4> nfcGpioPins{SS, SCK, MISO, MOSI};
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(misc_config_t, deviceName, otaPasswd, hk_key_color, setupCode, lockAlwaysUnlock, lockAlwaysLock, controlPin, hsStatusPin, nfcSuccessPin, nfcSuccessTime, nfcNeopixelPin, neopixelSuccessColor, neopixelFailureColor, neopixelSuccessTime, neopixelFailTime, nfcSuccessHL, nfcFailPin, nfcFailTime, nfcFailHL, gpioActionPin, gpioActionLockState, gpioActionUnlockState, gpioActionMomentaryEnabled, gpioActionMomentaryTimeout, webAuthEnabled, webUsername, webPassword, nfcGpioPins)
uint8_t btrLowStatusThreshold = 10;
bool proxBatEnabled = false;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(misc_config_t, deviceName, otaPasswd, hk_key_color, setupCode, lockAlwaysUnlock, lockAlwaysLock, controlPin, hsStatusPin, nfcSuccessPin, nfcSuccessTime, nfcNeopixelPin, neopixelSuccessColor, neopixelFailureColor, neopixelSuccessTime, neopixelFailTime, nfcSuccessHL, nfcFailPin, nfcFailTime, nfcFailHL, gpioActionPin, gpioActionLockState, gpioActionUnlockState, gpioActionMomentaryEnabled, gpioActionMomentaryTimeout, webAuthEnabled, webUsername, webPassword, nfcGpioPins, btrLowStatusThreshold, proxBatEnabled)
} miscConfig;
};

KeyFlow hkFlow = KeyFlow::kFlowFAST;
SpanCharacteristic* lockCurrentState;
SpanCharacteristic* lockTargetState;
SpanCharacteristic* statusLowBtr;
SpanCharacteristic* btrLevel;
esp_mqtt_client_handle_t client = nullptr;

std::unique_ptr<Pixel> pixel;
Expand All @@ -155,6 +161,15 @@ bool save_to_nvs() {
return !set_nvs && !commit_nvs;
}

struct PhysicalLockBattery : Service::BatteryService
{
PhysicalLockBattery() {
LOG(D, "Configuring PhysicalLockBattery");
statusLowBtr = new Characteristic::StatusLowBattery(0, true);
btrLevel = new Characteristic::BatteryLevel(100, true);
}
};

struct LockManagement : Service::LockManagement
{
SpanCharacteristic* lockControlPoint;
Expand Down Expand Up @@ -682,6 +697,9 @@ void mqtt_connected_event(void* event_handler_arg, esp_event_base_t event_base,
if (espConfig::mqttData.lockEnableCustomState) {
esp_mqtt_client_subscribe(client, espConfig::mqttData.lockCustomStateCmd.c_str(), 0);
}
if (espConfig::miscConfig.proxBatEnabled) {
esp_mqtt_client_subscribe(client, espConfig::mqttData.btrLvlCmdTopic.c_str(), 0);
}
esp_mqtt_client_subscribe(client, espConfig::mqttData.lockStateCmd.c_str(), 0);
esp_mqtt_client_subscribe(client, espConfig::mqttData.lockCStateCmd.c_str(), 0);
esp_mqtt_client_subscribe(client, espConfig::mqttData.lockTStateCmd.c_str(), 0);
Expand Down Expand Up @@ -709,6 +727,13 @@ void mqtt_data_handler(void* event_handler_arg, esp_event_base_t event_base, int
lockCurrentState->setVal(state);
esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockCurrentState->getVal()).c_str(), 0, 1, true);
}
} else if (!strcmp(espConfig::mqttData.btrLvlCmdTopic.c_str(), topic.c_str())) {
btrLevel->setVal(state);
if (state <= espConfig::miscConfig.btrLowStatusThreshold) {
statusLowBtr->setVal(1);
} else {
statusLowBtr->setVal(0);
}
}
}

Expand Down Expand Up @@ -800,6 +825,10 @@ String miscHtmlProcess(const String& var) {
return String(espConfig::miscConfig.nfcGpioPins[2]);
} else if (var == "NFCMOSIGPIOPIN") {
return String(espConfig::miscConfig.nfcGpioPins[3]);
} else if (var == "BTRLOWTHRESHOLD") {
return String(espConfig::miscConfig.btrLowStatusThreshold);
} else if (var == "PROXBATENABLE") {
return String(espConfig::miscConfig.proxBatEnabled);
}
return String();
}
Expand Down Expand Up @@ -883,6 +912,8 @@ String mqttHtmlProcess(const String& var) {
return String(espConfig::mqttData.customLockStates["C_UNKNOWN"]);
} else if (var == "NFCTAGSNOPUBLISH") {
return String(espConfig::mqttData.nfcTagNoPublish);
} else if (var == "BTRLEVELCMD") {
return String(espConfig::mqttData.btrLvlCmdTopic.c_str());
}
return "";
}
Expand Down Expand Up @@ -1122,6 +1153,17 @@ void setupWeb() {
return;
}
espConfig::miscConfig.nfcGpioPins[3] = p->value().toInt();
} else if (!strcmp(p->name().c_str(), "prox-bat-enable")) {
espConfig::miscConfig.proxBatEnabled = p->value().toInt();
} else if (!strcmp(p->name().c_str(), "btr-low-threshold")) {
espConfig::miscConfig.btrLowStatusThreshold = p->value().toInt();
if (statusLowBtr && btrLevel) {
if (btrLevel->getVal() <= espConfig::miscConfig.btrLowStatusThreshold) {
statusLowBtr->setVal(1);
} else {
statusLowBtr->setVal(0);
}
}
}
}
json json_misc_config = espConfig::miscConfig;
Expand Down Expand Up @@ -1614,7 +1656,20 @@ void setup() {
}
save_to_nvs();
});

new SpanUserCommand('N', "Btr status low", [](const char* arg) {
const char* TAG = "BTR_LOW";
if (strncmp(arg + 1, "0", 1) == 0) {
statusLowBtr->setVal(0);
LOG(I, "Low status set to NORMAL");
} else if (strncmp(arg + 1, "1", 1) == 0) {
statusLowBtr->setVal(1);
LOG(I, "Low status set to LOW");
}
});
new SpanUserCommand('B', "Btr level", [](const char* arg) {
uint8_t level = atoi(static_cast<const char *>(arg + 1));
btrLevel->setVal(level);
});

new SpanAccessory();
new Service::AccessoryInformation();
Expand All @@ -1640,6 +1695,9 @@ void setup() {
new NFCAccess();
new Service::HAPProtocolInformation();
new Characteristic::Version();
if (espConfig::miscConfig.proxBatEnabled) {
new PhysicalLockBattery();
}
homeSpan.setControllerCallback(pairCallback);
homeSpan.setWifiCallback(wifiCallback);
if (espConfig::miscConfig.nfcNeopixelPin != 255) {
Expand Down

0 comments on commit df3bc49

Please sign in to comment.