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
61 changes: 61 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,67 @@ jobs:
. $IDF_PATH/export.sh
idf.py size

# Local control without the mDNS announcement. Keeps the UDP listener but
# drops the mdns component, which is the configuration for a board whose app
# partition cannot take it. A gate that is never built is a gate that breaks.
build-no-mdns:
runs-on: ubuntu-latest
container:
image: espressif/idf:v5.1
strategy:
fail-fast: false
matrix:
idf-target: [esp32c3, esp32]
example: [contact_sensor, switch]

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: 'recursive'

- name: Clean managed components
run: |
find . -type d -name "managed_components" -exec rm -rf {} + || true

- name: Build ${{ matrix.example }} for ${{ matrix.idf-target }} without mDNS
working-directory: examples/${{ matrix.example }}
run: |
. $IDF_PATH/export.sh
echo "CONFIG_SINRICPRO_LOCAL_CONTROL_NO_MDNS=y" >> sdkconfig.defaults
idf.py set-target ${{ matrix.idf-target }}
idf.py build

# Local control compiled out entirely. This variant has never been proven by a
# full link, so CI is the place that does it.
build-no-local-control:
runs-on: ubuntu-latest
container:
image: espressif/idf:v5.1
strategy:
fail-fast: false
matrix:
idf-target: [esp32, esp32c3]
example: [switch]

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: 'recursive'

- name: Clean managed components
run: |
find . -type d -name "managed_components" -exec rm -rf {} + || true

- name: Build ${{ matrix.example }} for ${{ matrix.idf-target }} without local control
working-directory: examples/${{ matrix.example }}
run: |
. $IDF_PATH/export.sh
echo "CONFIG_SINRICPRO_ENABLE_LOCAL_CONTROL=n" >> sdkconfig.defaults
idf.py set-target ${{ matrix.idf-target }}
idf.py build

lint:
runs-on: ubuntu-latest
steps:
Expand Down
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,71 @@
# Changelog

## [1.3.0]

### Features

- feat: local control. The device answers signed SinricPro commands over the LAN
(UDP 3333, multicast 224.9.9.9, unicast too), so it keeps responding to the app
while the cloud is unreachable. Requests are dispatched through the same
capability callbacks as cloud requests.
- feat: mDNS announcement of `_sinricpro._udp.local.` as `sinricpro-<mac>`, with
TXT records `deviceIds`, `sdk` and `udp=1`, refreshed when the device list changes.
- feat: Kconfig gates `SINRICPRO_ENABLE_LOCAL_CONTROL` (default on) and
`SINRICPRO_LOCAL_CONTROL_NO_MDNS` (UDP without the announcement).
- feat: `sinricpro_local_control_is_running()`.

### Fixes

- fix: an unreachable cloud no longer aborts `sinricpro_start()`. Only an invalid
configuration is fatal; a connect failure logs, leaves the reconnect armed and
keeps local control serving. Callers check `sinricpro_is_connected()`.
- fix: outgoing messages are signed over the exact bytes transmitted. The payload
is serialised once and spliced into the envelope instead of being serialised a
second time, and the signature is emitted last so a receiver can slice it out.
- fix: a message with no signature, or with a payload that could not be located,
is no longer processed as if it had verified.
- fix: a request that fails verification now gets a signed "Signature is invalid"
response instead of silence, so a client can tell a wrong app secret from an
unreachable device.
- fix: signatures are compared in constant time.
- fix: payload extraction no longer treats a brace inside a JSON string as the end
of the payload.
- fix: `sinricpro_core_send_event()` no longer leaks the caller's value object when
it returns early because the SDK is stopped or the cloud is down.


| | |
|---|---|
| Transport | UDP port 3333, multicast group 224.9.9.9, unicast to the device too |
| Envelope | Identical to the cloud format, HMAC-SHA256 over the payload, base64 |
| Discovery | mDNS `_sinricpro._udp.local.`, host `sinricpro-<mac>` |
| TXT records | `deviceIds=<csv>`, `sdk=<version>`, `udp=1` |

Check it is up with `sinricpro_local_control_is_running()`. It is independent of
`sinricpro_is_connected()`: a device that has never reached SinricPro still
answers the LAN.

Verify from a desktop on the same network:

```bash
avahi-browse -r _sinricpro._udp # Linux
dns-sd -B _sinricpro._udp # macOS / Windows
```

### Notes

- The listener runs as its own FreeRTOS task (~6 KB stack by default). Device
callbacks execute on it, so size it for your own callbacks.
- The mDNS responder costs roughly 40 KB of flash. The default 2 MB single-app
partition layout has no room for it - the examples set
`CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y`.
- A request that fails signature verification gets a signed
"Signature is invalid" reply rather than silence, so a client can tell a wrong
app secret from an unreachable device.
- Android clients need a `WifiManager.MulticastLock` or mDNS returns nothing;
iOS clients need the service type in `NSBonjourServices`.


## [1.2.1]

### Fixes
Expand Down
26 changes: 19 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
# Requirements are resolved during early expansion, where sdkconfig values are
# already available; the mdns component is only pulled in when it is used.
set(SINRICPRO_REQUIRES
esp_websocket_client
mbedtls
esp_event
nvs_flash
esp_netif
esp_wifi
cjson
)

if(CONFIG_SINRICPRO_ENABLE_LOCAL_CONTROL AND NOT CONFIG_SINRICPRO_LOCAL_CONTROL_NO_MDNS)
list(APPEND SINRICPRO_REQUIRES espressif__mdns)
endif()

idf_component_register(
SRCS
"src/core/sinricpro_core.c"
"src/core/sinricpro_websocket.c"
"src/core/sinricpro_signature.c"
"src/core/sinricpro_message_queue.c"
"src/core/sinricpro_event_limiter.c"
"src/core/sinricpro_udp.c"
"src/core/sinricpro_mdns.c"
"src/devices/sinricpro_switch.c"
"src/devices/sinricpro_motion_sensor.c"
"src/devices/sinricpro_contact_sensor.c"
Expand Down Expand Up @@ -47,11 +65,5 @@ idf_component_register(
INCLUDE_DIRS
"include"
REQUIRES
esp_websocket_client
mbedtls
esp_event
nvs_flash
esp_netif
esp_wifi
cjson
${SINRICPRO_REQUIRES}
)
56 changes: 56 additions & 0 deletions Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,60 @@ menu "SinricPro Configuration"
help
Interval for sending heartbeat/ping messages to server.

config SINRICPRO_ENABLE_LOCAL_CONTROL
bool "Enable local control (LAN/UDP)"
default y
help
Answer signed SinricPro commands received over the LAN, so the
device keeps responding to the app while the cloud is unreachable.

Listens on UDP SINRICPRO_UDP_PORT, joined to the SinricPro
multicast group and also answering unicast. Requests are dispatched
through the same capability callbacks as cloud requests.

config SINRICPRO_LOCAL_CONTROL_NO_MDNS
bool "Disable the mDNS announcement"
default n
depends on SINRICPRO_ENABLE_LOCAL_CONTROL
help
Keep the UDP listener but do not publish _sinricpro._udp.local.
The device is then only reachable at the address the cloud reports
for it, and cannot be discovered on the LAN.

Saves the flash and RAM cost of the mdns component.

config SINRICPRO_UDP_PORT
int "Local control UDP port"
default 3333
range 1 65535
depends on SINRICPRO_ENABLE_LOCAL_CONTROL
help
Wire contract with the SinricPro app. Only change this if the app
has been configured to match.

config SINRICPRO_UDP_MULTICAST_IP
string "Local control multicast group"
default "224.9.9.9"
depends on SINRICPRO_ENABLE_LOCAL_CONTROL
help
Wire contract with the SinricPro app. Only change this if the app
has been configured to match.

config SINRICPRO_UDP_TASK_STACK_SIZE
int "Local control task stack size"
default 6144
depends on SINRICPRO_ENABLE_LOCAL_CONTROL
help
Stack for the UDP listener task. Device callbacks run on this task,
on top of signature verification and JSON parsing, so lower it only
after measuring the high-water mark of your own callbacks.

config SINRICPRO_UDP_TASK_PRIORITY
int "Local control task priority"
default 5
range 1 24
depends on SINRICPRO_ENABLE_LOCAL_CONTROL
help
Priority of the UDP listener task.

endmenu
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Official ESP-IDF component for [SinricPro](https://sinric.pro) - Control your ES

- ✅ **Voice Control** - Works with Alexa and Google Home
- ✅ **Real-time** - WebSocket-based bidirectional communication
- ✅ **Local Control** - Answers the app over the LAN when the cloud is down
- ✅ **Secure** - HMAC-SHA256 message signatures
- ✅ **Reliable** - Auto-reconnection and heartbeat monitoring
- ✅ **Event-driven** - ESP event loop integration
Expand Down Expand Up @@ -53,6 +54,26 @@ All devices below have complete API support and working examples:
- ✅ Power Sensor - Voltage, current, power monitoring
- ✅ Window AC - Air conditioner with fan speed and temperature

## Local Control (LAN)

The device answers signed SinricPro commands received over the LAN, so the app
keeps working when the cloud is unreachable. It is on by default and needs no
code changes: LAN requests are dispatched through the same capability callbacks
as cloud requests.

### Configuration

`idf.py menuconfig` → *Component config* → *SinricPro Configuration*:

| Option | Default | Effect |
|---|---|---|
| `SINRICPRO_ENABLE_LOCAL_CONTROL` | on | Compile local control in |
| `SINRICPRO_LOCAL_CONTROL_NO_MDNS` | off | Keep UDP, drop the announcement (and the `mdns` dependency) |
| `SINRICPRO_UDP_PORT` | 3333 | Wire contract with the app |
| `SINRICPRO_UDP_MULTICAST_IP` | 224.9.9.9 | Wire contract with the app |
| `SINRICPRO_UDP_TASK_STACK_SIZE` | 6144 | Device callbacks run on this task |
| `SINRICPRO_UDP_TASK_PRIORITY` | 5 | |

## Requirements

- ESP-IDF v4.4 or higher. Tested on ESP-IDF 6.1
Expand All @@ -67,7 +88,7 @@ Add to your project's `idf_component.yml`:

```yaml
dependencies:
sinricpro/esp-idf: "^1.2.1"
sinricpro/esp-idf: "^1.3.1"
```

### Method 2: Manual Installation
Expand All @@ -80,7 +101,7 @@ git clone https://github.com/sinricpro/esp-idf.git sinricpro
Or

```bash
idf.py add-dependency "sinricpro/esp-idf^1.2.1"
idf.py add-dependency "sinricpro/esp-idf^1.3.1"
```

View at: https://components.espressif.com/components/sinricpro/esp-idf
Expand Down
4 changes: 4 additions & 0 deletions examples/air_quality_sensor/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/blinds/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/contact_sensor/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/dimswitch/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/fan/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/garage_door/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/light/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/lock/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/motion_sensor/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/power_sensor/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/speaker/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/switch/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/temperature_sensor/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/thermostat/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/tv/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
4 changes: 4 additions & 0 deletions examples/windowac/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The default 2 MB / single-app layout leaves no room once local control pulls
# in the mdns responder.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
6 changes: 5 additions & 1 deletion idf_component.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: "1.2.1"
version: "1.3.0"
description: "SinricPro IoT platform integration for ESP-IDF - Control ESP32 devices with Alexa and Google Home"
url: "https://github.com/sinricpro/esp-idf"
documentation: "https://help.sinric.pro"
Expand All @@ -12,6 +12,10 @@ dependencies:
version: "^1.2.0"
espressif/cjson:
version: "*"
# Local control announcement. Always fetched; only linked when
# CONFIG_SINRICPRO_ENABLE_LOCAL_CONTROL is set and ..._NO_MDNS is not.
espressif/mdns:
version: "^1.8.0"

tags:
- iot
Expand Down
Loading
Loading