Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xiaomi gateway 3 support #36

Merged
merged 2 commits into from
Jun 27, 2024
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
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
Loading