diff --git a/fs_src/index.html b/fs_src/index.html index 92b33c81..f3992f6f 100644 --- a/fs_src/index.html +++ b/fs_src/index.html @@ -354,6 +354,7 @@

Input

+
@@ -403,6 +404,7 @@

Disabled Input

+
@@ -430,6 +432,7 @@

Sensor

+
diff --git a/fs_src/script.js b/fs_src/script.js index 4bea6662..42e181f7 100644 --- a/fs_src/script.js +++ b/fs_src/script.js @@ -421,12 +421,15 @@ function findOrAddContainer(cd) { case 7: // Motion Sensor. case 8: // Occupancy Sensor. case 9: // Contact Sensor. + case 10: // Doorbell c = el("sensor_template").cloneNode(true); c.id = elId; el(c, "save_btn").onclick = function () { mosSetConfig(c); }; break; + default: + console.log(`Unhandled component type: ${cd.type}`); } if (c) { c.style.display = "block"; @@ -566,6 +569,7 @@ function updateComponent(cd) { case 7: // Motion Sensor case 8: // Occupancy Sensor case 9: // Contact Sensor + case 10: // Doorbell var headText = `Input ${cd.id}`; if (cd.name) headText += ` (${cd.name})`; el(c, "head").innerText = headText; @@ -582,6 +586,8 @@ function updateComponent(cd) { } el(c, "status").innerText = statusText; break; + default: + console.log(`Unhandled component type: ${cd.type}`); } c.data = cd; } diff --git a/src/shelly_common.hpp b/src/shelly_common.hpp index 7688e247..d38a7177 100644 --- a/src/shelly_common.hpp +++ b/src/shelly_common.hpp @@ -35,6 +35,7 @@ #define SHELLY_HAP_AID_BASE_OCCUPANCY_SENSOR 0x700 #define SHELLY_HAP_AID_BASE_CONTACT_SENSOR 0x800 #define SHELLY_HAP_AID_BASE_VALVE 0x900 +#define SHELLY_HAP_AID_BASE_DOORBELL 0xa00 #define SHELLY_HAP_IID_BASE_SWITCH 0x100 #define SHELLY_HAP_IID_STEP_SWITCH 4 @@ -54,6 +55,7 @@ #define SHELLY_HAP_IID_STEP_SENSOR 0x10 #define SHELLY_HAP_IID_BASE_VALVE 0xa00 #define SHELLY_HAP_IID_STEP_VALVE 0x10 +#define SHELLY_HAP_IID_BASE_DOORBELL 0xb00 namespace shelly { diff --git a/src/shelly_component.hpp b/src/shelly_component.hpp index f38c947e..4e7a97a8 100644 --- a/src/shelly_component.hpp +++ b/src/shelly_component.hpp @@ -34,6 +34,7 @@ class Component { kMotionSensor = 7, kOccupancySensor = 8, kContactSensor = 9, + kDoorbell = 10, kMax, }; diff --git a/src/shelly_hap_doorbell.cpp b/src/shelly_hap_doorbell.cpp new file mode 100644 index 00000000..c8b33860 --- /dev/null +++ b/src/shelly_hap_doorbell.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) Shelly-HomeKit Contributors + * All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "shelly_hap_doorbell.hpp" + +#include "HAPUUID.h" + +namespace shelly { +namespace hap { + +const HAPUUID kHAPServiceType_Doorbell = HAPUUIDCreateAppleDefined(0x121); + +Doorbell::Doorbell(int id, Input *in, struct mgos_config_in_ssw *cfg) + : StatelessSwitchBase(id, in, cfg, SHELLY_HAP_IID_BASE_DOORBELL, + &kHAPServiceType_Doorbell, "service.doorbell") { +} + +Doorbell::~Doorbell() { +} + +Component::Type Doorbell::type() const { + return Type::kDoorbell; +} + +} // namespace hap +} // namespace shelly \ No newline at end of file diff --git a/src/shelly_hap_doorbell.hpp b/src/shelly_hap_doorbell.hpp new file mode 100644 index 00000000..9b35a841 --- /dev/null +++ b/src/shelly_hap_doorbell.hpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) Shelly-HomeKit Contributors + * All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include "shelly_hap_stateless_switch_base.hpp" + +namespace shelly { +namespace hap { + +class Doorbell : public StatelessSwitchBase { + public: + Doorbell(int id, Input *in, struct mgos_config_in_ssw *cfg); + virtual ~Doorbell(); + + // Component interface impl. + Type type() const override; +}; + +} // namespace hap +} // namespace shelly \ No newline at end of file diff --git a/src/shelly_hap_input.cpp b/src/shelly_hap_input.cpp index 24d41cbb..d59bb3cb 100644 --- a/src/shelly_hap_input.cpp +++ b/src/shelly_hap_input.cpp @@ -21,6 +21,7 @@ #include "mgos_hap.h" #include "shelly_hap_contact_sensor.hpp" +#include "shelly_hap_doorbell.hpp" #include "shelly_hap_motion_sensor.hpp" #include "shelly_hap_occupancy_sensor.hpp" #include "shelly_hap_stateless_switch.hpp" @@ -132,6 +133,13 @@ Status ShellyInput::Init() { s_ = cs; break; } + case Type::kDoorbell: { + auto *db = new hap::Doorbell(id(), in_, + (struct mgos_config_in_ssw *) &cfg_->ssw); + c_.reset(db); + s_ = db; + break; + } default: { return mgos::Errorf(STATUS_INVALID_ARGUMENT, "Invalid type %d", (int) initial_type_); @@ -195,6 +203,8 @@ uint16_t ShellyInput::GetAIDBase() const { return SHELLY_HAP_AID_BASE_OCCUPANCY_SENSOR; case Type::kContactSensor: return SHELLY_HAP_AID_BASE_CONTACT_SENSOR; + case Type::kDoorbell: + return SHELLY_HAP_AID_BASE_DOORBELL; default: return 0; } @@ -212,6 +222,7 @@ bool ShellyInput::IsValidType(int type) { case (int) Type::kMotionSensor: case (int) Type::kOccupancySensor: case (int) Type::kContactSensor: + case (int) Type::kDoorbell: return true; } return false; diff --git a/src/shelly_hap_stateless_switch.cpp b/src/shelly_hap_stateless_switch.cpp index 89e3f35f..7fc4debb 100644 --- a/src/shelly_hap_stateless_switch.cpp +++ b/src/shelly_hap_stateless_switch.cpp @@ -17,158 +17,23 @@ #include "shelly_hap_stateless_switch.hpp" -#include "mgos.hpp" -#include "mgos_hap.hpp" - namespace shelly { namespace hap { StatelessSwitch::StatelessSwitch(int id, Input *in, struct mgos_config_in_ssw *cfg) - : Component(id), - Service( - // IDs used to start at 0, preserve compat. - (SHELLY_HAP_IID_BASE_STATELESS_SWITCH + - (SHELLY_HAP_IID_STEP_STATELESS_SWITCH * (id - 1))), + : StatelessSwitchBase( + id, in, cfg, SHELLY_HAP_IID_BASE_STATELESS_SWITCH, &kHAPServiceType_StatelessProgrammableSwitch, - kHAPServiceDebugDescription_StatelessProgrammableSwitch), - in_(in), - cfg_(cfg) { + kHAPServiceDebugDescription_StatelessProgrammableSwitch) { } StatelessSwitch::~StatelessSwitch() { - in_->RemoveHandler(handler_id_); } Component::Type StatelessSwitch::type() const { return Type::kStatelessSwitch; } -std::string StatelessSwitch::name() const { - return cfg_->name; -} - -Status StatelessSwitch::Init() { - if (in_ == nullptr) { - return mgos::Errorf(STATUS_INVALID_ARGUMENT, "input is required"); - } - uint16_t iid = svc_.iid + 1; - // Name - AddNameChar(iid++, cfg_->name); - // Programmable Switch Event - AddChar(new mgos::hap::UInt8Characteristic( - iid++, &kHAPCharacteristicType_ProgrammableSwitchEvent, 0, 2, 1, - [this](HAPAccessoryServerRef *, const HAPUInt8CharacteristicReadRequest *, - uint8_t *value) { - if (last_ev_ts_ == 0) return kHAPError_InvalidState; - *value = last_ev_; - return kHAPError_None; - }, - true /* supports_notification */, nullptr /* write_handler */, - kHAPCharacteristicDebugDescription_ProgrammableSwitchEvent)); - - handler_id_ = in_->AddHandler( - std::bind(&StatelessSwitch::InputEventHandler, this, _1, _2)); - - return Status::OK(); -} - -StatusOr StatelessSwitch::GetInfo() const { - double last_ev_age = -1; - if (last_ev_ts_ > 0) { - last_ev_age = mgos_uptime() - last_ev_ts_; - } - return mgos::SPrintf("st:%d m:%d lea: %.3f", in_->GetState(), cfg_->in_mode, - last_ev_age); -} - -StatusOr StatelessSwitch::GetInfoJSON() const { - double last_ev_age = -1; - if (last_ev_ts_ > 0) { - last_ev_age = mgos_uptime() - last_ev_ts_; - } - return mgos::JSONPrintStringf( - "{id: %d, type: %d, name: %Q, in_mode: %d, " - "last_ev: %d, last_ev_age: %.3f}", - id(), type(), (cfg_->name ? cfg_->name : ""), cfg_->in_mode, last_ev_, - last_ev_age); -} - -Status StatelessSwitch::SetConfig(const std::string &config_json, - bool *restart_required) { - char *name = nullptr; - int in_mode = -1; - json_scanf(config_json.c_str(), config_json.size(), "{name: %Q, in_mode: %d}", - &name, &in_mode); - mgos::ScopedCPtr name_owner(name); - // Validation. - if (name != nullptr && strlen(name) > 64) { - return mgos::Errorf(STATUS_INVALID_ARGUMENT, "invalid %s", - "name (too long, max 64)"); - } - if (in_mode < 0 || in_mode > 2) { - return mgos::Errorf(STATUS_INVALID_ARGUMENT, "invalid %s", "in_mode"); - } - // Now copy over. - if (name != nullptr && strcmp(name, cfg_->name) != 0) { - mgos_conf_set_str(&cfg_->name, name); - *restart_required = true; - } - cfg_->in_mode = in_mode; - return Status::OK(); -} - -Status StatelessSwitch::SetState(const std::string &state_json) { - (void) state_json; - return Status::UNIMPLEMENTED(); -} - -void StatelessSwitch::InputEventHandler(Input::Event ev, bool state) { - const auto in_mode = static_cast(cfg_->in_mode); - switch (in_mode) { - // In momentary input mode we translate input events to HAP events directly. - case InMode::kMomentary: - switch (ev) { - case Input::Event::kSingle: - RaiseEvent( - kHAPCharacteristicValue_ProgrammableSwitchEvent_SinglePress); - break; - case Input::Event::kDouble: - RaiseEvent( - kHAPCharacteristicValue_ProgrammableSwitchEvent_DoublePress); - break; - case Input::Event::kLong: - RaiseEvent(kHAPCharacteristicValue_ProgrammableSwitchEvent_LongPress); - break; - case Input::Event::kChange: - case Input::Event::kReset: - case Input::Event::kMax: - // Ignore. - break; - } - break; - // In toggle switch input mode we translate changes to HAP events. - case InMode::kToggleShort: - case InMode::kToggleShortLong: - if (ev != Input::Event::kChange) break; - if (in_mode == InMode::kToggleShortLong && !state) { - RaiseEvent(kHAPCharacteristicValue_ProgrammableSwitchEvent_DoublePress); - } else { - RaiseEvent(kHAPCharacteristicValue_ProgrammableSwitchEvent_SinglePress); - } - break; - } -} - -void StatelessSwitch::RaiseEvent(uint8_t ev) { - last_ev_ = ev; - last_ev_ts_ = mgos_uptime(); - LOG(LL_INFO, ("Input %d: HAP event (mode %d): %d", id(), cfg_->in_mode, ev)); - // May happen during init, we don't want to raise events until initialized. - if (handler_id_ != Input::kInvalidHandlerID) { - chars_[1]->RaiseEvent(); - } -} - } // namespace hap -} // namespace shelly +} // namespace shelly \ No newline at end of file diff --git a/src/shelly_hap_stateless_switch.hpp b/src/shelly_hap_stateless_switch.hpp index 657bc6e1..8d33f095 100644 --- a/src/shelly_hap_stateless_switch.hpp +++ b/src/shelly_hap_stateless_switch.hpp @@ -16,60 +16,19 @@ */ #pragma once - -#include -#include - -#include "mgos_hap.hpp" -#include "mgos_sys_config.h" -#include "mgos_timers.h" - -#include "shelly_common.hpp" -#include "shelly_component.hpp" -#include "shelly_input.hpp" -#include "shelly_output.hpp" -#include "shelly_pm.hpp" +#include "shelly_hap_stateless_switch_base.hpp" namespace shelly { namespace hap { -// Common base for Switch, Outlet and Lock services. -class StatelessSwitch : public Component, public mgos::hap::Service { +class StatelessSwitch : public StatelessSwitchBase { public: - enum class InMode { - kMomentary = 0, - kToggleShort = 1, - kToggleShortLong = 2, - }; - StatelessSwitch(int id, Input *in, struct mgos_config_in_ssw *cfg); virtual ~StatelessSwitch(); // Component interface impl. - Status Init() override; Type type() const override; - std::string name() const override; - StatusOr GetInfo() const override; - StatusOr GetInfoJSON() const override; - Status SetConfig(const std::string &config_json, - bool *restart_required) override; - Status SetState(const std::string &state_json) override; - - private: - void InputEventHandler(Input::Event ev, bool state); - - void RaiseEvent(uint8_t ev); - - Input *const in_; - struct mgos_config_in_ssw *cfg_; - - Input::HandlerID handler_id_ = Input::kInvalidHandlerID; - - uint8_t last_ev_ = 0; - double last_ev_ts_ = 0; - - StatelessSwitch(const StatelessSwitch &other) = delete; }; } // namespace hap -} // namespace shelly +} // namespace shelly \ No newline at end of file diff --git a/src/shelly_hap_stateless_switch_base.cpp b/src/shelly_hap_stateless_switch_base.cpp new file mode 100644 index 00000000..12c44c9e --- /dev/null +++ b/src/shelly_hap_stateless_switch_base.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (c) Shelly-HomeKit Contributors + * All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "shelly_hap_stateless_switch_base.hpp" + +#include "mgos.hpp" +#include "mgos_hap.hpp" + +namespace shelly { +namespace hap { + +StatelessSwitchBase::StatelessSwitchBase(int id, Input *in, + struct mgos_config_in_ssw *cfg, + uint16_t iid_base, const HAPUUID *type, + const char *debug_description) + : Component(id), + Service( + // IDs used to start at 0, preserve compat. + (iid_base + (SHELLY_HAP_IID_STEP_STATELESS_SWITCH * (id - 1))), type, + debug_description), + in_(in), + cfg_(cfg) { +} + +StatelessSwitchBase::~StatelessSwitchBase() { + in_->RemoveHandler(handler_id_); +} + +Component::Type StatelessSwitchBase::type() const { + return Type::kStatelessSwitch; +} + +std::string StatelessSwitchBase::name() const { + return cfg_->name; +} + +Status StatelessSwitchBase::Init() { + if (in_ == nullptr) { + return mgos::Errorf(STATUS_INVALID_ARGUMENT, "input is required"); + } + uint16_t iid = svc_.iid + 1; + // Name + AddNameChar(iid++, cfg_->name); + // Programmable Switch Event + AddChar(new mgos::hap::UInt8Characteristic( + iid++, &kHAPCharacteristicType_ProgrammableSwitchEvent, 0, 2, 1, + [this](HAPAccessoryServerRef *, const HAPUInt8CharacteristicReadRequest *, + uint8_t *value) { + if (last_ev_ts_ == 0) return kHAPError_InvalidState; + *value = last_ev_; + return kHAPError_None; + }, + true /* supports_notification */, nullptr /* write_handler */, + kHAPCharacteristicDebugDescription_ProgrammableSwitchEvent)); + + handler_id_ = in_->AddHandler( + std::bind(&StatelessSwitchBase::InputEventHandler, this, _1, _2)); + + return Status::OK(); +} + +StatusOr StatelessSwitchBase::GetInfo() const { + double last_ev_age = -1; + if (last_ev_ts_ > 0) { + last_ev_age = mgos_uptime() - last_ev_ts_; + } + return mgos::SPrintf("st:%d m:%d lea: %.3f", in_->GetState(), cfg_->in_mode, + last_ev_age); +} + +StatusOr StatelessSwitchBase::GetInfoJSON() const { + double last_ev_age = -1; + if (last_ev_ts_ > 0) { + last_ev_age = mgos_uptime() - last_ev_ts_; + } + return mgos::JSONPrintStringf( + "{id: %d, type: %d, name: %Q, in_mode: %d, " + "last_ev: %d, last_ev_age: %.3f}", + id(), type(), (cfg_->name ? cfg_->name : ""), cfg_->in_mode, last_ev_, + last_ev_age); +} + +Status StatelessSwitchBase::SetConfig(const std::string &config_json, + bool *restart_required) { + char *name = nullptr; + int in_mode = -1; + json_scanf(config_json.c_str(), config_json.size(), "{name: %Q, in_mode: %d}", + &name, &in_mode); + mgos::ScopedCPtr name_owner(name); + // Validation. + if (name != nullptr && strlen(name) > 64) { + return mgos::Errorf(STATUS_INVALID_ARGUMENT, "invalid %s", + "name (too long, max 64)"); + } + if (in_mode < 0 || in_mode > 2) { + return mgos::Errorf(STATUS_INVALID_ARGUMENT, "invalid %s", "in_mode"); + } + // Now copy over. + if (name != nullptr && strcmp(name, cfg_->name) != 0) { + mgos_conf_set_str(&cfg_->name, name); + *restart_required = true; + } + cfg_->in_mode = in_mode; + return Status::OK(); +} + +Status StatelessSwitchBase::SetState(const std::string &state_json) { + (void) state_json; + return Status::UNIMPLEMENTED(); +} + +void StatelessSwitchBase::InputEventHandler(Input::Event ev, bool state) { + const auto in_mode = static_cast(cfg_->in_mode); + switch (in_mode) { + // In momentary input mode we translate input events to HAP events directly. + case InMode::kMomentary: + switch (ev) { + case Input::Event::kSingle: + RaiseEvent( + kHAPCharacteristicValue_ProgrammableSwitchEvent_SinglePress); + break; + case Input::Event::kDouble: + RaiseEvent( + kHAPCharacteristicValue_ProgrammableSwitchEvent_DoublePress); + break; + case Input::Event::kLong: + RaiseEvent(kHAPCharacteristicValue_ProgrammableSwitchEvent_LongPress); + break; + case Input::Event::kChange: + case Input::Event::kReset: + case Input::Event::kMax: + // Ignore. + break; + } + break; + // In toggle switch input mode we translate changes to HAP events. + case InMode::kToggleShort: + case InMode::kToggleShortLong: + if (ev != Input::Event::kChange) break; + if (in_mode == InMode::kToggleShortLong && !state) { + RaiseEvent(kHAPCharacteristicValue_ProgrammableSwitchEvent_DoublePress); + } else { + RaiseEvent(kHAPCharacteristicValue_ProgrammableSwitchEvent_SinglePress); + } + break; + } +} + +void StatelessSwitchBase::RaiseEvent(uint8_t ev) { + last_ev_ = ev; + last_ev_ts_ = mgos_uptime(); + LOG(LL_INFO, ("Input %d: HAP event (mode %d): %d", id(), cfg_->in_mode, ev)); + // May happen during init, we don't want to raise events until initialized. + if (handler_id_ != Input::kInvalidHandlerID) { + chars_[1]->RaiseEvent(); + } +} + +} // namespace hap +} // namespace shelly diff --git a/src/shelly_hap_stateless_switch_base.hpp b/src/shelly_hap_stateless_switch_base.hpp new file mode 100644 index 00000000..1845233f --- /dev/null +++ b/src/shelly_hap_stateless_switch_base.hpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) Shelly-HomeKit Contributors + * All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "mgos_hap.hpp" +#include "mgos_sys_config.h" +#include "mgos_timers.h" + +#include "shelly_common.hpp" +#include "shelly_component.hpp" +#include "shelly_input.hpp" +#include "shelly_output.hpp" +#include "shelly_pm.hpp" + +namespace shelly { +namespace hap { + +// Common base for Switch, Outlet and Lock services. +class StatelessSwitchBase : public Component, public mgos::hap::Service { + public: + enum class InMode { + kMomentary = 0, + kToggleShort = 1, + kToggleShortLong = 2, + }; + + StatelessSwitchBase(int id, Input *in, struct mgos_config_in_ssw *cfg, + uint16_t iid_base, const HAPUUID *type, + const char *debug_description); + virtual ~StatelessSwitchBase(); + + // Component interface impl. + Status Init() override; + Type type() const override; + std::string name() const override; + StatusOr GetInfo() const override; + StatusOr GetInfoJSON() const override; + Status SetConfig(const std::string &config_json, + bool *restart_required) override; + Status SetState(const std::string &state_json) override; + + private: + void InputEventHandler(Input::Event ev, bool state); + + void RaiseEvent(uint8_t ev); + + Input *const in_; + struct mgos_config_in_ssw *cfg_; + + Input::HandlerID handler_id_ = Input::kInvalidHandlerID; + + uint8_t last_ev_ = 0; + double last_ev_ts_ = 0; + + StatelessSwitchBase(const StatelessSwitchBase &other) = delete; +}; + +} // namespace hap +} // namespace shelly