Skip to content

Commit

Permalink
Add a README.md file
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinchalet committed Dec 7, 2024
1 parent 4270749 commit b6fa785
Show file tree
Hide file tree
Showing 2 changed files with 290 additions and 1 deletion.
3 changes: 2 additions & 1 deletion OpenNetty.sln
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{0570A3E6-8
global.json = global.json
LICENSE.md = LICENSE.md
NuGet.config = NuGet.config
README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{B2C1F565-2CCD-47FD-941C-4F01871186EA}"
Expand All @@ -37,7 +38,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{B2C1F565-2CC
eng\_._ = eng\_._
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{AFFA1C2A-7331-40BC-B633-367F8FBF317E}"
ProjectSection(SolutionItems) = preProject
.github\workflows\build.yml = .github\workflows\build.yml
EndProjectSection
Expand Down
288 changes: 288 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
# OpenNetty

## What is OpenNetty?

OpenNetty aims at providing an **advanced solution** for implementing [OpenWebNet](https://en.wikipedia.org/wiki/OpenWebNet)
support in .NET 8.0+ applications.

OpenWebNet is a protocol developed by [BTicino](https://www.bticino.it/) and [Legrand](https://www.legrand.fr/) in 2000 to manage electrical
networks. While it uses a very basic wire format, the OpenWebNet protocol is actually fairly complex to implement properly but also quite powerful.

To this date, 3 variants of OpenWebNet have been developed by the two companies:
- OpenWebNet, used to integrate with the [SCS](https://en.wikipedia.org/wiki/Bus_SCS)-based "MyHome" products.
- OpenWebNet/Nitoo, used to integrate with "In One by Legrand" products (powerline and radio).
- OpenWebNet/Zigbee, used to integrate with the Zigbee-based "MyHome Play" products.

> [!NOTE]
> OpenNetty is currently the only library that supports the 3 OpenWebNet variants.
OpenNetty offers both low-level primitives to represent OpenWebNet messages and communicate with OpenWebNet
gateways and a higher-level MQTT integration that can be directly used with home automation software like
[Home Assistant](https://www.home-assistant.io/), [Jeedom](https://jeedom.com/) or [FHEM](https://fhem.de/).

> [!IMPORTANT]
> **An OpenWebNet gateway is required by OpenNetty to be able to interact with BTicino and Legrand devices**:
>
> - For In One by Legrand devices, a Legrand 88213 powerline/USB gateway is required. To communicate with
> In One by Legrand radio devices, a Legrand 03606 interface must also be present in the installation.
> - For MyHome/MyHome Up devices, both BTicino F454 and MH202 SCS/Ethernet gateways are currently supported.
> - For MyHome Play devices, a BTicino 3578 or Legrand 88328 Zigbee/USB gateway is required.
>
> Since the "In One by Legrand" and "MyHome Play" products are no longer manufactured, buying the corresponding gateway
> is generally not easy (or cheap!), so this should probably only be considered for large existing installations.
--------------

## Core components

### Primitives

To represent raw OpenWebNet frames, OpenNetty exposes 3 low-level structures – `OpenNettyFrame`, `OpenNettyField` and `OpenNettyParameter` – and
one high-level primitive – `OpenNettyMessage` – that can be used to represent any message type supported by the 3 OpenWebNet specifications:
- BUS COMMAND
- DIMENSION REQUEST
- DIMENSION READ
- DIMENSION SET
- STATUS REQUEST

```csharp
var message = OpenNettyMessage.CreateCommand(
protocol: OpenNettyProtocol.Nitoo,
command : OpenNettyCommands.Lighting.On,
address : OpenNettyAddress.FromNitooAddress(identifier: 487932, unit: 2),
media : OpenNettyMedia.Powerline,
mode : OpenNettyMode.Unicast);
```

```csharp
var message = OpenNettyMessage.CreateFromFrame(OpenNettyProtocol.Nitoo, "*1*1*7806914##");

// (487932, 2)
var (identifier, unit) = OpenNettyAddress.ToNitooAddress(message.Address!.Value);
```

### Sessions and connections

The `OpenNettySession` class is the main entry point for **manually communicating** with an OpenWebNet gateway: it takes care
of initializing the connection and negotiates the desired OpenWebNet session type automatically. If authentication is
required by the remote gateway, it also takes care of the authentication dance in a completely transparent way.

`OpenNettySession` implements `IAsyncObservable<OpenNettyMessage>` and can be natively used with any of the extensions
provided by the [System.Reactive.Async package](https://www.nuget.org/packages/System.Reactive.Async) to filter and
observe the messages sent by the OpenWebNet gateway.

```csharp
var gateway = OpenNettyGateway.Create(
name : "SCS-Ethernet gateway",
brand : OpenNettyBrand.BTicino,
model : "F454",
endpoint: IPEndPoint.Parse("192.168.5.10:20000"),
password: "aJhYiBHk8");

await using var session = await OpenNettySession.CreateAsync(gateway, OpenNettySessionType.Event);

await using var subscription = await session.SubscribeAsync(message => Console.WriteLine(message.ToString()));
await using var connection = await session.ConnectAsync();

await Task.Delay(-1);
```

> [!TIP]
> For advanced scenarios that only involve sequential processing, the `OpenNettyConnection` class can also be directly
> used to send and/or receive OpenWebNet frames from a gateway using either a TCP connection or a serial port:
>
> ```csharp
> using var port = new SerialPort(
> portName: "/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0",
> baudRate: 19_200,
> parity : Parity.None,
> dataBits: 8,
> stopBits: StopBits.One);
>
> await using var connection = await OpenNettyConnection.CreateSerialConnectionAsync(port);
>
> var message = OpenNettyMessage.CreateCommand(
> protocol: OpenNettyProtocol.Zigbee,
> command : OpenNettyCommands.Lighting.On,
> address : OpenNettyAddress.FromHexadecimalZigbeeAddress(identifier: "0065ACAC", unit: 1),
> media : OpenNettyMedia.Radio,
> mode : OpenNettyMode.Unicast);
>
> await connection.SendAsync(message.Frame, CancellationToken.None);
>
> if (await connection.ReceiveAsync(CancellationToken.None) != OpenNettyFrames.Acknowledgement)
> {
> throw new ApplicationException("The frame was not acknowledged by the gateway.");
> }
> ```
### .NET Generic Host integration
While **sessions and connections can be directly used to communicate with an OpenWebNet gateway, it is not the recommended approach**.
Instead, **users are strongly encouraged to leverage OpenNetty's .NET Generic Host integration**: it will register a worker
for each configured gateway and will dynamically manage sessions, process incoming messages and dispatch outgoing messages.
It also automatically retransmit failed outgoing messages using a retry policy defined by OpenNetty based on the type of gateway.
Once the OpenNetty services are registered using the dedicated `.AddOpenNetty()` extension, the low-level `IOpenNettyService`
interface can be leveraged to execute any arbitrary BUS COMMAND, DIMENSION REQUEST, DIMENSION SET or STATUS REQUEST operation
and return the corresponding response returned by the gateway, if applicable:
```csharp
var builder = Host.CreateApplicationBuilder();
builder.Services.AddSystemd()
.AddWindowsService();
builder.Services.AddOpenNetty(options =>
{
// Register the SCS gateway used to communicate with MyHome devices.
options.AddGateway(OpenNettyGateway.Create(
name : "F454 gateway",
brand : OpenNettyBrand.BTicino,
model : "F454",
endpoint: IPEndPoint.Parse("192.168.5.10:20000"),
password: "aJhYiBHk8"));
});
var app = builder.Build();
await app.StartAsync();
// Send a WHO=13/DIMENSION=19 request and extract the raw values resolved from the response returned by the gateway.
var service = app.Services.GetRequiredService<IOpenNettyService>();
var values = await service.GetDimensionAsync(OpenNettyProtocol.Scs, OpenNettyDimensions.Management.Uptime);
await app.StopAsync();
```
> [!TIP]
> While the `IOpenNettyService` service can be very useful to send arbitrary messages or observe specific incoming messages, defining endpoints
> as shown in the next section and using the strongly-typed APIs offered by `OpenNettyController` when possible is strongly recommended.
### Endpoints

**For all its high-level operations, OpenNetty relies on the `OpenNettyEndpoint` class**:

- An endpoint generally has an address associated (but it's not always true, as gateway endpoints don't have an address attached).
- In most cases, an endpoint has a device definition attached from which it resolves the supported functions (like switching on or
off a connected load or controlling the brightness level), but it's possible to create endpoints that don't have a device
attached, which allows supporting non-device-specific addresses like SCS point-of-light area or group addresses.
- Nitoo and Zigbee endpoints often have a unit definition attached, but non-unit-specific endpoints can also
be created to perform actions that don't target a specific Nitoo or Zigbee unit (e.g Nitoo device descriptions).
- When no unit or device definition is attached, a list of capabilities must be attached
to the endpoint before being able to perform actions using the `OpenNettyController` class.

```csharp
var builder = Host.CreateApplicationBuilder();

builder.Services.AddSystemd()
.AddWindowsService();

builder.Services.AddOpenNetty(options =>
{
options.AddGateway(OpenNettyGateway.Create(
name : "F454 gateway",
brand : OpenNettyBrand.BTicino,
model : "F454",
endpoint: IPEndPoint.Parse("192.168.5.10:20000"),
password: "aJhYiBHk8"));

options.AddEndpoint(new OpenNettyEndpoint
{
Address = OpenNettyAddress.FromScsLightPointPointToPointAddress(area: 1, point: 3),
Device = new OpenNettyDevice
{
Definition = OpenNettyDevices.GetDeviceByModel(OpenNettyBrand.BTicino, "F418U2")
?? throw new InvalidOperationException("The specified gateway model is not supported.")
},
Name = "Bathroom/Recessed light",
Protocol = OpenNettyProtocol.Scs
});

options.AddEndpoint(new OpenNettyEndpoint
{
Address = OpenNettyAddress.FromScsLightPointAreaAddress(area: 1),
Capabilities = [OpenNettyCapabilities.OnOffSwitching],
Name = "Bathroom/All lights",
Protocol = OpenNettyProtocol.Scs
});
});

var app = builder.Build();
await app.StartAsync();

var manager = app.Services.GetRequiredService<OpenNettyManager>();
var controller = app.Services.GetRequiredService<OpenNettyController>();

// Resolve the brightness of the dimmable recessed light in area 1.
var brightness = await controller.GetBrightnessAsync(
await manager.FindEndpointByNameAsync("Bathroom/Recessed light")
?? throw new InvalidOperationException("The endpoint couldn't be resolved."));

// Switch off all the lights located in area 1.
await controller.SwitchOffAsync(
await manager.FindEndpointByNameAsync("Bathroom/All lights")
?? throw new InvalidOperationException("The endpoint couldn't be resolved."));

await app.StopAsync();
```

### Events

To infer high-level state changes affecting registered endpoints, OpenNetty includes a built-in `OpenNettyCoordinator` service that monitors
all the incoming messages sent by the configured gateways and invokes the corresponding events exposed by the `OpenNettyEvents` class.

By implementing the `IOpenNettyHandler` interface, it is possible to subscribe to any event before incoming frames start being processed:

```csharp
var builder = Host.CreateApplicationBuilder();

builder.Services.AddSystemd()
.AddWindowsService();

builder.Services.AddOpenNetty(options =>
{
var file = builder.Environment.ContentRootFileProvider.GetFileInfo("OpenNettyConfiguration.xml");
options.ImportFromXmlConfiguration(file);
});

builder.Services.AddSingleton<IOpenNettyHandler, MyEventHandler>();

var app = builder.Build();
await app.RunAsync();

class MyEventHandler(OpenNettyEvents events) : IOpenNettyHandler
{
public async ValueTask<IAsyncDisposable> SubscribeAsync() => StableCompositeAsyncDisposable.Create(
[
await events.BrightnessReported
.Where(args => !string.IsNullOrEmpty(args.Endpoint.Name))
.SubscribeAsync(args => Console.WriteLine($"Brightness on endpoint {args.Endpoint.Name}: {args.Level}.")),

await events.SwitchStateReported
.Where(args => !string.IsNullOrEmpty(args.Endpoint.Name))
.SubscribeAsync(args => Console.WriteLine($"Switch state on endpoint {args.Endpoint.Name}:" +
(args.State is OpenNettyModels.Lighting.SwitchState.On ? "on" : "off")))
]);
}
```

--------------

## Security policy

Security issues and bugs should be reported privately by emailing [email protected].
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message.

--------------

## Contributors

**OpenNetty** is actively maintained by **[Kévin Chalet](https://github.com/kevinchalet)**. Contributions are welcome and can be submitted using pull requests.

--------------

## License

This project is licensed under the **Apache License**. This means that you can use, modify and distribute it freely.
See [http://www.apache.org/licenses/LICENSE-2.0.html](http://www.apache.org/licenses/LICENSE-2.0.html) for more details.

0 comments on commit b6fa785

Please sign in to comment.