Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions components/ws_bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,55 @@ build/dashboard tooling produces):

- `on_connected` — the WebSocket connected and Home Assistant accepted the connection
- `on_disconnected` — the connection was lost
- `on_declare` — entity declarations are being (re)sent: on connect **and** on
every periodic re-announce. Use this for hand-built declarations (see below),
not `on_connected`.

## Declaring entity types ESPHome has no domain for

Home Assistant supports entity types ESPHome itself has no platform for — the
main one being `device_tracker` (GPS location). Since there's no ESPHome
`device_tracker:` domain to hang a `platform: ws_bridge` off, declare these by
calling the protocol directly from a lambda:

```yaml
ws_bridge:
id: my_ws_bridge
host: !secret ha_address
token: !secret ha_token

on_declare:
- lambda: |-
id(my_ws_bridge)->send_entity_declare(
"car_location", "device_tracker", "Car Location", "", "", nullptr);

interval:
- interval: 30s
then:
- lambda: |-
id(my_ws_bridge)->send_state_object("car_location", [](JsonObject v) {
v["latitude"] = id(my_gps).latitude;
v["longitude"] = id(my_gps).longitude;
v["gps_accuracy"] = 8;
});
```

- **Declare from `on_declare:`, not `on_connected:`.** Declarations are re-sent
both on connect and on each periodic re-announce (see below); `on_connected`
only covers the former, so a re-announce that heals a lost Home Assistant-side
registration would silently leave your hand-built entity behind.
- `send_state_object()` is for states that aren't a single scalar —
`device_tracker` needs latitude and longitude together. For ordinary values
use `send_state_float()` / `send_state_bool()` / `send_state_string()`.
- `send_entity_declare()`'s last argument adds platform-specific declare fields;
pass `nullptr` when there are none, or a lambda taking a `JsonObject` (that's
how `select` sends its `options`, `number` its `min`/`max`/`step`, and so on).
- Everything is a plain no-op while disconnected, so these are safe to call from
any interval or trigger.

See the integration's
[PROTOCOL.md](https://github.com/eigger/hass-ws-bridge/blob/main/docs/PROTOCOL.md)
for the full set of declarable platforms and their fields.

## Behavior / Limitations

Expand Down
14 changes: 14 additions & 0 deletions components/ws_bridge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
CONF_WS_DEVICE_NAME,
CONF_ON_CONNECTED,
CONF_ON_DISCONNECTED,
CONF_ON_DECLARE,
CONF_PING_INTERVAL,
CONF_PONG_TIMEOUT,
CONF_RECONNECT_TIMEOUT,
Expand All @@ -35,6 +36,7 @@

ConnectedTrigger = ws_bridge_ns.class_("ConnectedTrigger", automation.Trigger.template())
DisconnectedTrigger = ws_bridge_ns.class_("DisconnectedTrigger", automation.Trigger.template())
DeclareTrigger = ws_bridge_ns.class_("DeclareTrigger", automation.Trigger.template())


def _validate_esp_idf(config):
Expand Down Expand Up @@ -82,6 +84,15 @@ def _validate_esp_idf(config):
cv.Optional(CONF_ON_DISCONNECTED): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisconnectedTrigger)}
),
# Fires alongside the registered platform entities' own
# re-declaration — on connect and on every re-announce. This is
# where hand-built send_entity_declare() calls belong (e.g. for
# entity types ESPHome has no domain for, like device_tracker);
# on_connected would skip the re-announce and leave them missing
# after HA-side registration is healed.
cv.Optional(CONF_ON_DECLARE): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DeclareTrigger)}
),
}
).extend(cv.COMPONENT_SCHEMA),
_validate_esp_idf,
Expand Down Expand Up @@ -134,3 +145,6 @@ async def to_code(config):
for conf in config.get(CONF_ON_DISCONNECTED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DECLARE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
9 changes: 9 additions & 0 deletions components/ws_bridge/automation.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,14 @@ class DisconnectedTrigger : public Trigger<> {
}
};

// Fires whenever entity declarations are (re)sent: on connect and on every
// periodic re-announce. Hook manual send_entity_declare() calls here.
class DeclareTrigger : public Trigger<> {
public:
explicit DeclareTrigger(WsBridgeComponent *parent) {
parent->add_on_declare_callback([this]() { this->trigger(); });
}
};

} // namespace ws_bridge
} // namespace esphome
1 change: 1 addition & 0 deletions components/ws_bridge/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

CONF_ON_CONNECTED = "on_connected"
CONF_ON_DISCONNECTED = "on_disconnected"
CONF_ON_DECLARE = "on_declare"

CONF_PING_INTERVAL = "ping_interval"
CONF_PONG_TIMEOUT = "pong_timeout"
Expand Down
10 changes: 10 additions & 0 deletions components/ws_bridge/ws_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ void WsBridgeComponent::route_command_(const WsCommand &command) {

void WsBridgeComponent::declare_all_entities_() {
for (auto *device : this->devices_) device->ws_bridge_declare();
// Manual (lambda-built) declarations piggyback here rather than on
// on_connected, so they're re-sent by the periodic re-announce too — see
// add_on_declare_callback().
this->declare_cb_.call();
}

void WsBridgeComponent::send_raw_(const std::string &msg) {
Expand Down Expand Up @@ -319,5 +323,11 @@ void WsBridgeComponent::send_state_string(const std::string &unique_id, const st
this->send_raw_(build_state_string(this->next_id_(), unique_id, value));
}

void WsBridgeComponent::send_state_object(const std::string &unique_id,
const std::function<void(JsonObject)> &value_fn) {
if (!this->is_connected()) return;
this->send_raw_(build_state_object(this->next_id_(), unique_id, value_fn));
}

} // namespace ws_bridge
} // namespace esphome
12 changes: 12 additions & 0 deletions components/ws_bridge/ws_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class WsBridgeComponent : public Component {

void add_on_connected_callback(std::function<void()> &&cb) { this->connected_cb_.add(std::move(cb)); }
void add_on_disconnected_callback(std::function<void()> &&cb) { this->disconnected_cb_.add(std::move(cb)); }
// Fires wherever the registered platform entities re-declare themselves —
// i.e. on connect AND on every periodic re-announce. Manual (lambda-built)
// declarations must hook this rather than on_connected, so that a
// re-announce which heals a lost HA-side registration re-declares them too.
void add_on_declare_callback(std::function<void()> &&cb) { this->declare_cb_.add(std::move(cb)); }

void setup() override;
void loop() override;
Expand All @@ -75,9 +80,15 @@ class WsBridgeComponent : public Component {
// Called by platform entities (via WsBridgeDevice helpers) to push state
// and declarations. No-ops while not connected; the next (re)connect will
// re-declare and re-push through ws_bridge_declare().
//
// These are also the escape hatch for declaring entity types ESPHome itself
// has no domain for (e.g. device_tracker): call them straight from a YAML
// lambda. Manually declared entities are NOT re-declared automatically on
// reconnect — drive them from the hub's on_connected: trigger. See README.
void send_state_float(const std::string &unique_id, float value);
void send_state_bool(const std::string &unique_id, bool value);
void send_state_string(const std::string &unique_id, const std::string &value);
void send_state_object(const std::string &unique_id, const std::function<void(JsonObject)> &value_fn);
void send_entity_declare(const std::string &unique_id, const std::string &platform, const std::string &name,
const std::string &device_id, const std::string &device_name,
const std::function<void(JsonObject)> &extra);
Expand Down Expand Up @@ -183,6 +194,7 @@ class WsBridgeComponent : public Component {

CallbackManager<void()> connected_cb_{};
CallbackManager<void()> disconnected_cb_{};
CallbackManager<void()> declare_cb_{};
};

} // namespace ws_bridge
Expand Down
13 changes: 13 additions & 0 deletions components/ws_bridge/ws_protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,18 @@ std::string build_state_string(uint32_t id, const std::string &unique_id, const
});
}

std::string build_state_object(uint32_t id, const std::string &unique_id,
const std::function<void(JsonObject)> &value_fn) {
return json::build_json([&](JsonObject root) {
root["id"] = id;
root["type"] = "ws_bridge/state";
JsonArray states = root["states"].to<JsonArray>();
JsonObject item = states.add<JsonObject>();
item["unique_id"] = unique_id;
JsonObject value = item["value"].to<JsonObject>();
if (value_fn) value_fn(value);
});
}

} // namespace ws_bridge
} // namespace esphome
6 changes: 6 additions & 0 deletions components/ws_bridge/ws_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,11 @@ std::string build_state_float(uint32_t id, const std::string &unique_id, float v
std::string build_state_bool(uint32_t id, const std::string &unique_id, bool value);
std::string build_state_string(uint32_t id, const std::string &unique_id, const std::string &value);

// State whose `value` is a JSON object rather than a scalar — for platforms
// that need several fields at once (device_tracker's latitude+longitude).
// `value_fn` is called with the (empty) value object to fill in.
std::string build_state_object(uint32_t id, const std::string &unique_id,
const std::function<void(JsonObject)> &value_fn);

} // namespace ws_bridge
} // namespace esphome
10 changes: 10 additions & 0 deletions tests/components/ws_bridge/test.esp32-idf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ wifi:
logger:

ws_bridge:
id: my_ws_bridge
host: 192.168.0.10
token: "test_token"
gateway_id: my_esp
Expand All @@ -28,6 +29,15 @@ ws_bridge:
- logger.log: "ws_bridge connected"
on_disconnected:
- logger.log: "ws_bridge disconnected"
# Hand-built declaration for an entity type ESPHome has no domain for.
on_declare:
- lambda: |-
id(my_ws_bridge)->send_entity_declare("car_location", "device_tracker", "Car Location", "", "", nullptr);
id(my_ws_bridge)->send_state_object("car_location", [](JsonObject v) {
v["latitude"] = 37.5665;
v["longitude"] = 126.9780;
v["gps_accuracy"] = 8;
});

sensor:
- platform: ws_bridge
Expand Down