Skip to content

Commit

Permalink
Merge pull request #36 from sergey-brutsky/xiaomi-gateway-3-support
Browse files Browse the repository at this point in the history
Xiaomi gateway 3 support
  • Loading branch information
sergey-brutsky authored Jun 27, 2024
2 parents 2b1f4e7 + 64c8673 commit 9fc74d5
Show file tree
Hide file tree
Showing 104 changed files with 5,758 additions and 1,876 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup dotnet core
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore -s https://api.nuget.org/v3/index.json
- name: Build
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2017 Sergey Brutsky
Copyright (c) 2017-2024 Sergey Brutsky

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ build:
dotnet build MiHomeLib -c Debug

run:
dotnet run -p MiHomeConsole
dotnet run --project MiHomeConsole

test:
dotnet test MiHomeUnitTests
Expand Down
33 changes: 10 additions & 23 deletions MiHomeConsole/Program.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,21 @@
using System;
using MiHomeLib;

namespace MiHomeConsole
namespace MiHomeConsole;
public class Program
{
public class Program
public static void Main()
{
public static void Main(string[] args)
using var gw3 = new XiaomiGateway3("<gateway ip>", "<gateway token>");
{
//Action<ILoggingBuilder> loggingBuilder =
// builder => builder.AddConsole(x =>
// {
// x.DisableColors = true;
// x.Format = ConsoleLoggerFormat.Systemd;
// x.TimestampFormat = " yyyy-MM-d [HH:mm:ss] - ";
// });

//MiHome.LoggerFactory = LoggerFactory.Create(loggingBuilder);
//MiHome.LogRawCommands = true;

// pwd of your gateway (optional, needed only to send commands to your devices)
// and sid of your gateway (optional, use only when you have 2 gateways in your LAN)
//using var miHome = new MiHome("pwd", "sid")
using var miHome = new MiHome();

miHome.OnAnyDevice += (_, device) =>
gw3.OnDeviceDiscovered += gw3SubDevice =>
{
Console.WriteLine($"{device.Sid}, {device.GetType()}, {device}"); // all discovered devices
Console.WriteLine(gw3SubDevice.ToString());
};

Console.ReadLine();
gw3.DiscoverDevices();
}

Console.ReadLine();
}
}
}
42 changes: 42 additions & 0 deletions MiHomeLib/ActionProcessors/AsyncBleEventMethodProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using MiHomeLib.DevicesV3;

namespace MiHomeLib.ActionProcessors;

public class AsyncBleEventMethodProcessor(Dictionary<string, XiaomiGateway3SubDevice> devices, ILoggerFactory loggerFactory) : IActionProcessor
{
public const string ACTION = "_async.ble_event";
private readonly Dictionary<string, XiaomiGateway3SubDevice> _devices = devices;
private readonly ILogger _logger = loggerFactory.CreateLogger<AsyncBleEventMethodProcessor>();

public void ProcessMessage(JsonNode json)
{
if (!json.AsObject().ContainsKey("params") || !json["params"].AsObject().ContainsKey("dev"))
{
_logger.LogWarning($"Json string --> '{json}' is not valid for ble parsing");
return;
}

var parms = json["params"].AsObject();
var dev = parms["dev"].AsObject();

if (!dev.ContainsKey("did"))
{
_logger.LogWarning($"json --> {json} has no 'did' property. Futher processing is impossible");
return;
}

var did = dev["did"].ToString();

if (!_devices.ContainsKey(did))
{
_logger.LogWarning($"Device with did '{did}' is unknown. Processing is skipped");
return;
}

_devices[did].LastTimeMessageReceived = parms["gwts"].GetValue<double>().UnixSecondsToDateTime();
_devices[did].ParseData(parms.ToString());
}
}
8 changes: 8 additions & 0 deletions MiHomeLib/ActionProcessors/IActionProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Text.Json.Nodes;

namespace MiHomeLib.ActionProcessors;

public interface IActionProcessor
{
void ProcessMessage(JsonNode json);
}
45 changes: 45 additions & 0 deletions MiHomeLib/ActionProcessors/ZigbeeHeartBeatCommandProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using MiHomeLib.DevicesV3;

namespace MiHomeLib.ActionProcessors;

public class ZigbeeHeartBeatCommandProcessor(Dictionary<string, XiaomiGateway3SubDevice> devices, ILoggerFactory loggerFactory) : IActionProcessor
{
public const string ACTION = "heartbeat";
private readonly Dictionary<string, XiaomiGateway3SubDevice> _devices = devices;
private readonly ILogger _logger = loggerFactory.CreateLogger<ZigbeeHeartBeatCommandProcessor>();

public void ProcessMessage(JsonNode json)
{
var data = json["params"].Deserialize<List<Dictionary<string, JsonElement>>>();

if (data.Count != 1)
{
_logger.LogWarning($"Wrong structure of heartbeat message --> '{data}'." +
"Processing of such structure is not supported");
return;
}

if (!data[0].ContainsKey("did"))
{
_logger.LogWarning("Heartbeat message doesn't contain 'did'." +
"Processing of such structure is not supported");
return;
}

var did = data[0]["did"].GetString();

if (_devices.ContainsKey(did))
{
_devices[did].LastTimeMessageReceived = data[0]["time"].GetDouble().UnixMilliSecondsToDateTime();
(_devices[did] as ZigBeeDevice).ParseData(data[0]["res_list"].ToString());
}
else
{
_logger.LogWarning($"Did '{did}' is unknown. Cannot process '{ACTION}' command for this device.");
}
}
}
34 changes: 34 additions & 0 deletions MiHomeLib/ActionProcessors/ZigbeeReportCommandProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using MiHomeLib.DevicesV3;

namespace MiHomeLib.ActionProcessors;

public class ZigbeeReportCommandProcessor: IActionProcessor
{
public const string ACTION = "report";
private readonly Dictionary<string, XiaomiGateway3SubDevice> _devices;
private readonly ILogger _logger;

public ZigbeeReportCommandProcessor(Dictionary<string, XiaomiGateway3SubDevice> devices, ILoggerFactory loggerFactory)
{
_devices = devices;
_logger = loggerFactory.CreateLogger(GetType());
}

public void ProcessMessage(JsonNode json)
{
var did = json["did"].ToString();

if (_devices.ContainsKey(did))
{
_devices[did].LastTimeMessageReceived = json["time"].GetValue<double>().UnixMilliSecondsToDateTime();
(_devices[did] as ZigBeeDevice).ParseData((json["mi_spec"] is not null ? json["mi_spec"] : json["params"]).ToString());
}
else
{
_logger.LogWarning($"Did '{did}' is unknown. Cannot process '{ACTION}' command for this device.");
}
}
}
6 changes: 6 additions & 0 deletions MiHomeLib/Devices/MiHomeDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public abstract class MiHomeDevice
{
public string Sid { get; }
public string Did { get; }
public string Name { get; set; }
public string Type { get; }

Expand All @@ -12,6 +13,11 @@ protected MiHomeDevice(string sid, string type)
Type = type;
}

protected MiHomeDevice(string did)
{
Did = did;
}

public abstract void ParseData(string command);

public override string ToString()
Expand Down
4 changes: 1 addition & 3 deletions MiHomeLib/Devices/Switch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace MiHomeLib.Devices
{
public class Switch : MiHomeDevice
public class Switch(string sid) : MiHomeDevice(sid, TypeKey)
{
public const string TypeKey = "switch";

Expand All @@ -13,8 +13,6 @@ public class Switch : MiHomeDevice

public event EventHandler OnLongPress;

public Switch(string sid) : base(sid, TypeKey) {}

public float? Voltage { get; set; }

public string Status { get; private set; }
Expand Down
12 changes: 12 additions & 0 deletions MiHomeLib/DevicesV3/AqaraDoorWindowSensor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.Extensions.Logging;

namespace MiHomeLib.DevicesV3;

// This sensor works exactly as XiaomiDoorWindowSensor, no need to repeat all stuff here
public class AqaraDoorWindowSensor(string did, ILoggerFactory loggerFactory)
: XiaomiDoorWindowSensor(did, loggerFactory)
{
public new const string MARKET_MODEL = "MCCGQ11LM";
public new const string MODEL = "lumi.sensor_magnet.aq2";
public override string ToString() => GetBaseInfo(MARKET_MODEL, MODEL) + $"Contact: {Contact}, " + GetBaseToString();
}
125 changes: 125 additions & 0 deletions MiHomeLib/DevicesV3/AqaraOneChannelRelayEu.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using MiHomeLib.Transport;

namespace MiHomeLib.DevicesV3;

public class AqaraOneChannelRelayEu : ZigBeeManageableDevice
{
public enum RelayState
{
Unknown = -1,
On = 1,
Off = 0,
}
public enum PowerMemoryState
{
PowerOff = 0,
Previous = 1,
}
public enum PowerMode
{
Momentary = 1,
Toggle = 2,
}
public const string MARKET_MODEL = "SSM-U01";
public const string MODEL = "lumi.switch.n0agl1";
private (int siid, int piid) STATE_RES = (2, 1);
private (int siid, int piid) LOAD_POWER_RES = (3, 2);
private (int siid, int piid) POWER_CONSUMPTION_RES = (3, 1);
private (int siid, int piid) POWER_MEMORY_RES = (5, 1);
private (int siid, int piid) POWER_MODE_RES = (7, 2);
private (int siid, int piid) POWER_OVERLOAD_RES = (5, 6);
private readonly Dictionary<(int siid, int piid), Action<JsonNode>> _actions;
public AqaraOneChannelRelayEu(string did, IMqttTransport mqttTransport, ILoggerFactory loggerFactory) : base(did, mqttTransport, loggerFactory)
{
_actions = new()
{
{LOAD_POWER_RES, x => // load power changed
{
var oldValue = LoadPower;
LoadPower = x.GetValue<float>();
OnLoadPowerChange?.Invoke(oldValue);
}
},
{STATE_RES, x => // channel state changed
{
var state = x.GetValue<bool>() ? 1 : 0;

if(state == (int)State) return; // No need to emit event when state is already actual

State = state == 1 ? RelayState.On : RelayState.Off;
OnStateChange?.Invoke();
}
},
{POWER_CONSUMPTION_RES, x => // electricity consumption changed
{
var oldValue = PowerConsumption;
PowerConsumption = x.GetValue<float>();
OnPowerConsumptionChange?.Invoke(oldValue);
}
},
};
}
public float LoadPower { get; internal set; }
public float PowerConsumption { get; internal set; }
public RelayState State { get; internal set; } = RelayState.Unknown;
public event Action OnStateChange;
/// <summary>
/// Old value passed as an argument in W
/// </summary>
public event Action<float> OnLoadPowerChange;
/// <summary>
/// Old value passed as an argument in kWh
/// </summary>
public event Action<float> OnPowerConsumptionChange;
protected internal override void ParseData(string data)
{
var listProps = JsonSerializer.Deserialize<List<JsonNode>>(data);

foreach (var prop in listProps)
{
var key = (prop["siid"].GetValue<int>(), prop["piid"].GetValue<int>());

if(_actions.ContainsKey(key))
_actions[key](prop["value"]);
}
}
public void PowerOn() => SendWriteCommand(STATE_RES, 1);
public void PowerOff() => SendWriteCommand(STATE_RES, 0);
public void ToggleState()
{
switch (State)
{
case RelayState.Off:
SendWriteCommand(STATE_RES, 1);
break;
case RelayState.On:
SendWriteCommand(STATE_RES, 0);
break;
};
}
public void SetPowerMemoryState(PowerMemoryState state) => SendWriteCommand(POWER_MEMORY_RES, (int)state);
public void SetPowerMode(PowerMode mode) => SendWriteCommand(POWER_MODE_RES, (int)mode);
/// <summary>
/// Warning ! Be very careful with this function.
/// If you set low threshold and it's reached, device will fall into "protection" mode and stop working
/// To reset this mode you need an external intervention (press button on the device or on external switch)
/// </summary>
public void SetPowerOverloadThreshold(int threshold)
{
if(threshold < 0 || threshold > 2200)
throw new ArgumentOutOfRangeException(nameof(threshold), threshold, $"Power overload threshold should be within range 1-2200 watt");

SendWriteCommand(POWER_OVERLOAD_RES, threshold);
}
public override string ToString()
{
return GetBaseInfo(MARKET_MODEL, MODEL) +
$"Load Power: {LoadPower}W, " +
$"Channel State: {State}";
}
}
Loading

0 comments on commit 9fc74d5

Please sign in to comment.