Pacem.Domotics.OpenWebNet
0.10.18-pascal
dotnet add package Pacem.Domotics.OpenWebNet --version 0.10.18-pascal
NuGet\Install-Package Pacem.Domotics.OpenWebNet -Version 0.10.18-pascal
<PackageReference Include="Pacem.Domotics.OpenWebNet" Version="0.10.18-pascal" />
<PackageVersion Include="Pacem.Domotics.OpenWebNet" Version="0.10.18-pascal" />
<PackageReference Include="Pacem.Domotics.OpenWebNet" />
paket add Pacem.Domotics.OpenWebNet --version 0.10.18-pascal
#r "nuget: Pacem.Domotics.OpenWebNet, 0.10.18-pascal"
#:package Pacem.Domotics.OpenWebNet@0.10.18-pascal
#addin nuget:?package=Pacem.Domotics.OpenWebNet&version=0.10.18-pascal&prerelease
#tool nuget:?package=Pacem.Domotics.OpenWebNet&version=0.10.18-pascal&prerelease
Pacem.Domotics.OpenWebNet
An OpenWebNet (BTicino / Legrand MyHOME) client: connect to a gateway, drive lights, dimmers, door locks and energy meters, and receive bus events as they happen.
Built on Pacem.Extensions.Sockets, so the link to the gateway survives reboots, cable pulls
and gateways that vanish for an afternoon.
Install
dotnet add package Pacem.Domotics.OpenWebNet
Targets net10.0.
Quick start
services.AddPacemOpenWebNet();
var gateways = provider.GetRequiredService<IOpenWebNetGatewayFactory>();
var devices = provider.GetRequiredService<IOpenWebNetDeviceFactory>();
using var gateway = gateways.CreateGateway("172.24.0.100", Gateway.DefaultBTicinoPort);
gateway.Events.OnOpenWebNetResponseAsync = ctx =>
{
if (ctx.Response is LightingResponse light)
{
Console.WriteLine($"light {light.Where} -> {light.LightStatus} ({light.Intensity:P0})");
}
return Task.CompletedTask;
};
await gateway.ConnectAsync();
var kitchen = (Light)devices.CreateOpenWebNetDevice(gateway, KnownDeviceType.Light, where: 11);
await kitchen.RequestStatusAsync();
await kitchen.SwitchOnAsync();
How it is wired
A Gateway holds two sockets to the same endpoint, as the protocol expects: a command
channel for requests and a listen channel for spontaneous bus events. Both keep themselves
alive and reconnect independently.
Devices are created against a gateway and register themselves for the messages addressed to
them. Incoming frames are routed by (WHO, WHERE) — so a light and an energy meter may
share an address without stealing each other's messages.
your code ──► IOpenWebNetDeviceFactory ──► Light / ConsumptionTracker / DoorEntry
│
▼
Gateway ──► command socket ──► BTicino gateway
◄── listen socket ◄──
Connecting is non-blocking
ConnectAsync() does not throw or hang when the gateway is unreachable — the underlying
connections retry in the background. Check IsSenderConnected / IsReceiverConnected:
await gateway.ConnectAsync();
if (!gateway.IsSenderConnected)
{
// still coming up; commands will be dropped with a logged error until it is
}
Gateways that ask for a password
A BTicino gateway lets clients in its trusted address range straight in; anyone else gets a
challenge right after the session opener — a numeric nonce, *#521725795## — and is dropped
unless it answers with the OPEN password scrambled by that nonce. Pass the password when
you ask for the gateway and the handshake is done for you, on both channels, on every reconnect:
using var gateway = gateways.CreateGateway("172.24.0.100", Gateway.DefaultBTicinoPort, password: "12345");
The password is a number (that is what the legacy exchange can scramble). A gateway that
answers with *98*2## wants HMAC-SHA256 instead, which is not implemented: the challenge is
logged and left unanswered. The exchange is the usual request/response pair — GatewayOpenPasswordPromptResponse
out of OpenWebNetResponse.Parse, GatewayOpenPasswordRequest with its Answer — should you
need it elsewhere. Note that the first CreateGateway for an endpoint decides its password —
the instance is cached and its sessions may already be open.
Devices
KnownDeviceType |
Type | Capabilities |
|---|---|---|
Light |
Light |
on / off / toggle / timed on / blink |
Dimmer |
Light (IsDimmable) |
the above, plus DimToAsync |
ConsumptionTracker |
ConsumptionTracker |
instantaneous, total, daily, monthly |
Door |
DoorEntry |
UnlockAsync |
var lamp = (Light)devices.CreateOpenWebNetDevice(gateway, KnownDeviceType.Dimmer, 12);
await lamp.DimToAsync(0.4); // 40%
await lamp.BlinkAsync(1.5); // 1.5s
var meter = (ConsumptionTracker)devices.CreateOpenWebNetDevice(gateway, KnownDeviceType.ConsumptionTracker, 7);
await meter.RequestCurrentAsync();
await meter.RequestMonthlyAsync(2026, 9);
var door = (DoorEntry)devices.CreateOpenWebNetDevice(gateway, KnownDeviceType.Door, 0);
await door.UnlockAsync();
Gateways and devices are cached per endpoint and per (gateway, type, where), so asking twice
gives you the same instance.
Device state (Light.Status, Light.Intensity, …) reflects the last message received, not
a live query — call RequestStatusAsync() to refresh it. Note that SwitchOnAsync /
SwitchOffAsync short-circuit when the cached state already matches.
Events
Set handlers on gateway.Events:
| Handler | Fires on |
|---|---|
OnOpenWebNetResponseAsync |
Every parsed frame from either channel |
OnOpenWebNetRequestAsync |
Every request sent |
OnConnectedAsync / OnDisconnectedAsync / OnCloseAsync |
Channel lifecycle |
OnErrorAsync |
Transport failures |
Each carries the CommunicationMode (Listen or Command) of the channel it came from.
Handlers run on the receive pump — keep them quick.
Parsing frames yourself
OpenWebNetResponse? response = OpenWebNetResponse.Parse("*1*1*11##");
IEnumerable<OpenWebNetResponse> many = OpenWebNetResponse.Extract("*1*1*11##*1*0*12##");
Parse takes one complete frame and returns null for frames whose WHO this library does not
model. Extract splits a buffer and discards a trailing incomplete frame — the Gateway
itself uses a stateful reader instead, so messages split across TCP reads are re-assembled
rather than lost.
Modelled today: lighting (WHO 1), energy (WHO 18) and acknowledgements. Other WHOs
reach OnOpenWebNetResponseAsync as null and are logged at trace level.
A note on WHO values
OpenWebNetWhoTable gathers the WHO constants. Two caveats worth carrying:
- Devices answer on the WHO given by their
KnownDeviceType, which is not always the WHO their commands go out on — door entry is the live example (DoorEntry= 6 vsVideoDoorEntry= 7). Gateway generations differ here. - Routing is by
(WHO, WHERE), so an incorrect WHO does not produce an error — it silently starves the device of its own messages. Confirm against your hardware rather than against the published tables.
License
Apache 2.0.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging (>= 10.0.12)
- Pacem.Domotics.Abstractions (>= 0.10.18-pascal)
- Pacem.Extensions.Sockets (>= 0.10.18-pascal)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.10.18-pascal | 48 | 9/21/2026 |
| 0.10.17 | 46 | 9/18/2026 |
| 0.10.17-foucault | 53 | 9/18/2026 |
| 0.10.16 | 53 | 9/16/2026 |
| 0.10.16-hamilton | 66 | 9/14/2026 |
| 0.10.16-gauss | 67 | 9/14/2026 |
| 0.10.16-faraday | 87 | 9/9/2026 |
| 0.10.15 | 86 | 9/7/2026 |