├── WyzeSenseBlazor ├── Pages │ ├── Index.razor │ ├── Error.cshtml.cs │ ├── _Host.cshtml │ ├── Error.cshtml │ ├── MQTTTemplates.razor │ └── WyzeSensors.razor ├── wwwroot │ ├── favicon.ico │ └── css │ │ ├── open-iconic │ │ ├── font │ │ │ ├── fonts │ │ │ │ ├── open-iconic.eot │ │ │ │ ├── open-iconic.otf │ │ │ │ ├── open-iconic.ttf │ │ │ │ └── open-iconic.woff │ │ │ └── css │ │ │ │ └── open-iconic-bootstrap.min.css │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── FONT-LICENSE │ │ └── site.css ├── .config │ └── dotnet-tools.json ├── appsettings.Development.json ├── DataServices │ ├── IMQTTTemplateService.cs │ ├── IMqttClientService.cs │ ├── MqttClientServiceProvider.cs │ ├── MQTTTemplateService.cs │ ├── IWyzeSenseService.cs │ ├── WyzeSenseService.cs │ └── MqttClientService.cs ├── Settings │ ├── WyzeSettings.cs │ ├── DatabaseSettings.cs │ ├── BrokerHostSettings.cs │ ├── ClientSettings.cs │ └── AppSettingsProvider.cs ├── DataStorage │ ├── IDataStoreOptions.cs │ ├── DataStoreOptions.cs │ ├── IDataStoreService.cs │ ├── DataStore.cs │ ├── Models │ │ ├── MQTT │ │ │ ├── Topics.cs │ │ │ ├── PayloadPackage.cs │ │ │ └── Template.cs │ │ └── WyzeSensorModel.cs │ └── DataStoreService.cs ├── App.razor ├── _Imports.razor ├── Shared │ ├── MainLayout.razor │ ├── NavMenu.razor.css │ ├── MainLayout.razor.css │ └── NavMenu.razor ├── Component │ └── WyzeSensor.razor ├── Mqtt │ └── Options │ │ └── AspCoreMqttClientOptionBuilder.cs ├── appsettings.json ├── WyzeSenseBlazor.csproj ├── datastore.json ├── Properties │ └── launchSettings.json ├── Program.cs ├── Views │ └── Shared │ │ └── _ValidationScriptsPartial.cshtml ├── WyzeLogger.cs ├── ServiceCollectionExtensions.cs └── Startup.cs ├── WyzeSenseUpgrade ├── firmware │ └── hms_cc1310.bin ├── WyzeSenseUpgrade.csproj └── Program.cs ├── WyzeSenseCore ├── WyzeSenseCore.csproj ├── Enums │ ├── WyzeEventType.cs │ ├── WyzeKeyPadState.cs │ └── WyzeSensorType.cs ├── WyzeSensor.cs ├── IWyzeSenseLogger.cs ├── TaskExtension.cs ├── WyzeSenseEvent.cs ├── WyzeDongleState.cs ├── IWyzeDongle.cs ├── DataProcessor.cs ├── ByteBuffer.cs ├── Packet.cs └── WyzeDongle.cs ├── WyzeSenseApp ├── WyzeSenseApp.csproj ├── Logger.cs └── Program.cs ├── README.md ├── WyzeSense.sln ├── .gitattributes └── .gitignore /WyzeSenseBlazor/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

Hello, world!

4 | 5 | Welcome to your new app. 6 | 7 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AK5nowman/WyzeSense/HEAD/WyzeSenseBlazor/wwwroot/favicon.ico -------------------------------------------------------------------------------- /WyzeSenseUpgrade/firmware/hms_cc1310.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AK5nowman/WyzeSense/HEAD/WyzeSenseUpgrade/firmware/hms_cc1310.bin -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AK5nowman/WyzeSense/HEAD/WyzeSenseBlazor/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AK5nowman/WyzeSense/HEAD/WyzeSenseBlazor/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AK5nowman/WyzeSense/HEAD/WyzeSenseBlazor/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AK5nowman/WyzeSense/HEAD/WyzeSenseBlazor/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /WyzeSenseUpgrade/WyzeSenseUpgrade.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "5.0.5", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /WyzeSenseBlazor/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataServices/IMQTTTemplateService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using WyzeSenseBlazor.DataStorage.Models; 3 | 4 | namespace WyzeSenseBlazor.DataServices 5 | { 6 | public interface IMQTTTemplateService 7 | { 8 | Task GetTemplatesAsync(); 9 | } 10 | } -------------------------------------------------------------------------------- /WyzeSenseBlazor/Settings/WyzeSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace WyzeSenseBlazor.Settings 7 | { 8 | public class WyzeSettings 9 | { 10 | public string UsbPath { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /WyzeSenseCore/WyzeSenseCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Settings/DatabaseSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace WyzeSenseBlazor.Settings 7 | { 8 | public class DatabaseSettings 9 | { 10 | public string DatabasePath { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataStorage/IDataStoreOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace WyzeSenseBlazor.DataStorage 7 | { 8 | public interface IDataStoreOptions 9 | { 10 | public string Path { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataStorage/DataStoreOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace WyzeSenseBlazor.DataStorage 7 | { 8 | public class DataStoreOptions: IDataStoreOptions 9 | { 10 | public string Path { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataStorage/IDataStoreService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace WyzeSenseBlazor.DataStorage 7 | { 8 | public interface IDataStoreService 9 | { 10 | public DataStore DataStore {get; } 11 | public Task Save(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Settings/BrokerHostSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace WyzeSenseBlazor.Settings 7 | { 8 | public class BrokerHostSettings 9 | { 10 | public string Host { set; get; } 11 | public int Port { set; get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /WyzeSenseCore/Enums/WyzeEventType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WyzeSenseCore 8 | { 9 | public enum WyzeEventType : byte 10 | { 11 | Alarm = 0xA2, 12 | Status = 0xA1, 13 | Climate = 0xE8, 14 | UserAction = 0x99, 15 | Unknown 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Settings/ClientSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace WyzeSenseBlazor.Settings 7 | { 8 | public class ClientSettings 9 | { 10 | public string Id { set; get; } 11 | public string UserName { set; get; } 12 | public string Password { set; get; } 13 | public string Topic { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using WyzeSenseBlazor 10 | @using WyzeSenseBlazor.Shared 11 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /WyzeSenseCore/Enums/WyzeKeyPadState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WyzeSenseCore 8 | { 9 | public enum WyzeKeyPadState : byte 10 | { 11 | Inactive = 0x00, 12 | Active = 0x01, 13 | Disarmed = 0x02, 14 | Home = 0x03, 15 | Away = 0x04, 16 | Alarm = 0x05, 17 | Unknown 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataStorage/DataStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using WyzeSenseBlazor.DataStorage.Models; 6 | 7 | namespace WyzeSenseBlazor.DataStorage 8 | { 9 | public class DataStore 10 | { 11 | public Dictionary Sensors { get; set; } = new(); 12 | public Dictionary Templates { get; set; } = new(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataStorage/Models/MQTT/Topics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.ComponentModel.DataAnnotations.Schema; 7 | 8 | namespace WyzeSenseBlazor.DataStorage.Models 9 | { 10 | public class Topics 11 | { 12 | public string RootTopic { get; set; } 13 | public string TemplateName { get; set; } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataServices/IMqttClientService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using MQTTnet.Client.Connecting; 3 | using MQTTnet.Client.Disconnecting; 4 | using MQTTnet.Client.Receiving; 5 | 6 | namespace WyzeSenseBlazor.DataServices 7 | { 8 | public interface IMqttClientService : IHostedService, 9 | IMqttClientConnectedHandler, 10 | IMqttClientDisconnectedHandler 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /WyzeSenseCore/Enums/WyzeSensorType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WyzeSenseCore 8 | { 9 | public enum WyzeSensorType : byte 10 | { 11 | Switch = 0x01, 12 | Motion = 0x02, 13 | Water = 0x03, 14 | KeyPad = 0x05, 15 | Climate = 0x07, 16 | SwitchV2 = 0x0E, 17 | Motionv2 = 0x0F, 18 | Unknown 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataStorage/Models/MQTT/PayloadPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.ComponentModel.DataAnnotations.Schema; 7 | namespace WyzeSenseBlazor.DataStorage.Models 8 | { 9 | public class PayloadPackage 10 | { 11 | public string Topic { get; set; } 12 | public Dictionary Payload { get; set; } 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Settings/AppSettingsProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace WyzeSenseBlazor.Settings 7 | { 8 | public class AppSettingsProvider 9 | { 10 | public static BrokerHostSettings BrokerHostSettings; 11 | public static ClientSettings ClientSettings; 12 | public static WyzeSettings WyzeSettings; 13 | public static DatabaseSettings DatabaseSettings; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataServices/MqttClientServiceProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace WyzeSenseBlazor.DataServices 7 | { 8 | public class MqttClientServiceProvider 9 | { 10 | public readonly IMqttClientService MqttClientService; 11 | 12 | public MqttClientServiceProvider(IMqttClientService mqttClientService) 13 | { 14 | MqttClientService = mqttClientService; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataStorage/Models/MQTT/Template.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.ComponentModel.DataAnnotations.Schema; 7 | 8 | namespace WyzeSenseBlazor.DataStorage.Models 9 | { 10 | public class Template 11 | { 12 | public string Name { get; set; } 13 | public int SensorType { get; set; } 14 | public List PayloadPackages { get; set; } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /WyzeSenseApp/WyzeSenseApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /WyzeSenseCore/WyzeSensor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WyzeSenseCore 8 | { 9 | public sealed class WyzeSensor 10 | { 11 | public string MAC; 12 | public WyzeSensorType Type; 13 | public byte Version; 14 | public WyzeSensor(ReadOnlySpan Data) 15 | { 16 | MAC = ASCIIEncoding.ASCII.GetString(Data.Slice(5, 8)); 17 | } 18 | public WyzeSensor() { } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Component/WyzeSensor.razor: -------------------------------------------------------------------------------- 1 | @using WyzeSenseCore 2 | @using AntDesign; 3 | 4 | @inherits FeedbackComponent 5 | 6 |
7 | 8 | @context.MAC 9 | 10 |
11 | 12 | @code { 13 | 14 | DataStorage.Models.WyzeSensorModel sensor { get; set; } 15 | protected override void OnInitialized() 16 | { 17 | sensor = base.Options; 18 | base.OnInitialized(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WyzeSenseCore/IWyzeSenseLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WyzeSenseCore 8 | { 9 | public interface IWyzeSenseLogger 10 | { 11 | public void Log(string level, string message); 12 | public void LogTrace(string message); 13 | public void LogDebug(string message); 14 | public void LogInformation(string message); 15 | public void LogWarning(string message); 16 | public void LogError(string message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Mqtt/Options/AspCoreMqttClientOptionBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using MQTTnet.Client.Options; 6 | 7 | namespace WyzeSenseBlazor.Mqtt.Options 8 | { 9 | public class AspCoreMqttClientOptionBuilder : MqttClientOptionsBuilder 10 | { 11 | public IServiceProvider ServiceProvider { get; } 12 | 13 | public AspCoreMqttClientOptionBuilder(IServiceProvider serviceProvider) 14 | { 15 | ServiceProvider = serviceProvider; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /WyzeSenseCore/TaskExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WyzeSenseCore 8 | { 9 | internal static class TaskExtension 10 | { 11 | public static async void FireAndForget(this Task task, IWyzeSenseLogger logger) 12 | { 13 | try 14 | { 15 | await task; 16 | } 17 | catch (Exception e) 18 | { 19 | logger.LogDebug($"[FireAndForget] Task Error: {e.ToString()}"); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WyzeSenseCore/WyzeSenseEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Buffers.Binary; 7 | 8 | namespace WyzeSenseCore 9 | { 10 | 11 | 12 | public sealed class WyzeSenseEvent 13 | { 14 | public WyzeSensor Sensor; 15 | 16 | public DateTime ServerTime; 17 | 18 | public WyzeEventType EventType; 19 | 20 | public Dictionary Data; 21 | 22 | public override string ToString() 23 | { 24 | return string.Format("{0} - {1}: {2}", Sensor.MAC, ServerTime, Sensor.Type); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataStorage/Models/WyzeSensorModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.ComponentModel.DataAnnotations.Schema; 7 | 8 | namespace WyzeSenseBlazor.DataStorage.Models 9 | { 10 | public class WyzeSensorModel 11 | { 12 | public string MAC { get; set; } 13 | public string Alias { get; set; } 14 | public string Description { get; set; } 15 | public int SensorType { get; set; } 16 | public DateTime LastActive {get; set; } 17 | public List Topics { get; set; } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information", 7 | "WyzeSenseCore.WyzeDongle": "Debug" 8 | } 9 | }, 10 | "WyzeSettings": { 11 | "UsbPath": "/dev/hidraw1" 12 | }, 13 | "DatabaseSettings": { 14 | "DatabasePath": "/path/to/datastore/datastore.json" 15 | }, 16 | "BrokerHostSettings": { 17 | "Host": "", 18 | "Port": 1883 19 | }, 20 | "ClientSettings": { 21 | "Id": "wyze2mqtt", 22 | "UserName": "", 23 | "Password": "", 24 | "Topic": "" 25 | }, 26 | "AllowedHosts": "*" 27 | } 28 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/WyzeSenseBlazor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | d2e87d8a-7d56-4c6c-a513-57669fa1e2dd 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/datastore.json: -------------------------------------------------------------------------------- 1 | {"Sensors":{"77BD78FC":{"MAC":"77BD78FC","Alias":"","Description":"","SensorType":0,"LastActive":"0001-01-01T00:00:00","Topics":null},"77C09F7D":{"MAC":"77C09F7D","Alias":"","Description":"","SensorType":0,"LastActive":"0001-01-01T00:00:00","Topics":null},"77C02EEE":{"MAC":"77C02EEE","Alias":"","Description":"","SensorType":0,"LastActive":"0001-01-01T00:00:00","Topics":null},"77AABB0B":{"MAC":"77AABB0B","Alias":"","Description":"","SensorType":0,"LastActive":"0001-01-01T00:00:00","Topics":null},"77D51E7B":{"MAC":"77D51E7B","Alias":"","Description":"","SensorType":0,"LastActive":"0001-01-01T00:00:00","Topics":null},"77D1D64A":{"MAC":"77D1D64A","Alias":"","Description":"","SensorType":0,"LastActive":"0001-01-01T00:00:00","Topics":null}},"Templates":{}} -------------------------------------------------------------------------------- /WyzeSenseBlazor/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:45904", 7 | "sslPort": 44368 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "WyzeSenseBlazor": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": "true", 21 | "launchBrowser": true, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WyzeSenseApp/Logger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace WyzeSenseApp 9 | { 10 | public class Logger: WyzeSenseCore.IWyzeSenseLogger 11 | { 12 | public void Log(string level, string message) 13 | { 14 | Console.WriteLine("[{0}] {1} {2}", level, DateTime.Now, message); 15 | } 16 | public void LogTrace(string message) => Log("Trace", message); 17 | public void LogInformation(string message) => Log("Information", message); 18 | public void LogDebug(string message) => Log("Debug", message); 19 | public void LogWarning(string message) => Log("Warning", message); 20 | public void LogError(string message) => Log("Error", message); 21 | 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataServices/MQTTTemplateService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using WyzeSenseBlazor.DataStorage.Models; 7 | using WyzeSenseBlazor.DataStorage; 8 | 9 | namespace WyzeSenseBlazor.DataServices 10 | { 11 | public class MQTTTemplateService : IMQTTTemplateService 12 | { 13 | private readonly ILogger _logger; 14 | private readonly IDataStoreService _dataStore; 15 | public MQTTTemplateService(ILogger logger, IDataStoreService dataStore) 16 | { 17 | _logger = logger; 18 | _dataStore = dataStore; 19 | } 20 | 21 | public async Task GetTemplatesAsync() 22 | { 23 | return _dataStore.DataStore.Templates.Values.ToArray(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /WyzeSenseCore/WyzeDongleState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WyzeSenseCore 8 | { 9 | public class WyzeDongleState 10 | { 11 | public byte[] ENR; 12 | public string MAC; 13 | public string Version; 14 | public byte DeviceType; 15 | public bool LEDState = true; 16 | public bool IsInclusive = false; 17 | public byte AuthState = 0; 18 | public override string ToString() 19 | { 20 | string enrbyteString = ""; 21 | for (int i = 0; i < ENR.Length; i++) 22 | { 23 | enrbyteString += string.Format("{0:X2} ", ENR[i]); 24 | } 25 | return string.Format($"{MAC}: AuthState: {AuthState}, Version: {Version}, DeviceType: {DeviceType}, LED State:{LEDState}, IsInclusive: {IsInclusive} ENR: {enrbyteString}"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WyzeSenseCore/IWyzeDongle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace WyzeSenseCore 9 | { 10 | public interface IWyzeDongle 11 | { 12 | event EventHandler OnAddSensor; 13 | event EventHandler OnRemoveSensor; 14 | event EventHandler OnSensorEvent; 15 | event EventHandler OnDongleStateChange; 16 | 17 | void Stop(); 18 | bool OpenDevice(string devicePath); 19 | Task StartAsync(CancellationToken cancellationToken); 20 | void SetLedAsync(bool On); 21 | Task StartScanAsync(int Timeout); 22 | Task StopScanAsync(); 23 | Task DeleteSensorAsync(string MAC); 24 | Task RequestRefreshSensorListAsync(); 25 | Task GetSensorAsync(); 26 | WyzeDongleState GetDongleState(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace WyzeSenseBlazor 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureLogging(logging => 22 | { 23 | logging.ClearProviders(); 24 | logging.AddConsole(); 25 | }) 26 | .ConfigureWebHostDefaults(webBuilder => 27 | { 28 | webBuilder.UseStartup(); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace WyzeSenseBlazor.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | [IgnoreAntiforgeryToken] 14 | public class ErrorModel : PageModel 15 | { 16 | public string RequestId { get; set; } 17 | 18 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 19 | 20 | private readonly ILogger _logger; 21 | 22 | public ErrorModel(ILogger logger) 23 | { 24 | _logger = logger; 25 | } 26 | 27 | public void OnGet() 28 | { 29 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | .content { 18 | padding-top: 1.1rem; 19 | } 20 | 21 | .valid.modified:not([type=checkbox]) { 22 | outline: 1px solid #26b050; 23 | } 24 | 25 | .invalid { 26 | outline: 1px solid red; 27 | } 28 | 29 | .validation-message { 30 | color: red; 31 | } 32 | 33 | #blazor-error-ui { 34 | background: lightyellow; 35 | bottom: 0; 36 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 37 | display: none; 38 | left: 0; 39 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 40 | position: fixed; 41 | width: 100%; 42 | z-index: 1000; 43 | } 44 | 45 | #blazor-error-ui .dismiss { 46 | cursor: pointer; 47 | position: absolute; 48 | right: 0.75rem; 49 | top: 0.5rem; 50 | } 51 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataServices/IWyzeSenseService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using WyzeSenseCore; 6 | using WyzeSenseBlazor.DataStorage.Models; 7 | 8 | namespace WyzeSenseBlazor.DataServices 9 | { 10 | public interface IWyzeSenseService 11 | { 12 | event EventHandler OnAddSensor; 13 | event EventHandler OnRemoveSensor; 14 | event EventHandler OnEvent; 15 | event EventHandler OnDongleStateChange; 16 | event EventHandler OnFailStart; 17 | 18 | bool Running { get; } 19 | 20 | void Stop(); 21 | void SetLEDOn(); 22 | void SetLEDOff(); 23 | Task StartScanAsync(int Timeout = 60 * 1000); 24 | Task StopScanAsync(); 25 | Task RequestDeleteSensor(string MAC); 26 | void RequestRefreshSensorListAsync(); 27 | Task GetSensorAsync(); 28 | WyzeDongleState GetDongleState(); 29 | 30 | Task SetAlias(string MAC, string Alias); 31 | Task SetDescription(string MAC, string Description); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WyzeSense 2 | Hobby Project to interface with Wyze V1 and V2 sensors/Keypad using the V1 Bridge. It may not be pretty but that is why know one is paying me for it :) 3 | Note: tested on ubuntu 20.04 4 | 5 | ------ 6 | #### WyzeSenseCore 7 | 8 | The heart of the project. 9 | Based on work from [HclX](https://github.com/HclX). Additional functionality found using [Ghidra](https://github.com/NationalSecurityAgency/ghidra). 10 | Certaintly needs some refactoring but that isn't nearly as fun as uncovering new features. 11 | 1. [HclX/WyzeSensePy](https://github.com/HclX/WyzeSensePy) 12 | 13 | ------ 14 | #### WyzeSenseUpgrade 15 | 16 | Console application to flash the hms cc1310 firmware to the v1 bridge. Use this at own risk - not all checks added. 17 | 1. [deeplyembeddedWP/cc2640r2f-sbl-linux](https://github.com/deeplyembeddedWP/cc2640r2f-sbl-linux) 18 | 2. [CC1310 Technical Reference Manual](https://www.ti.com/lit/ug/swcu117i/swcu117i.pdf?ts=1619413277495) 19 | 20 | ------ 21 | #### WyzeSenseApp 22 | 23 | Console application to test WyzeSenseCore functionality 24 | 25 | ------ 26 | #### WyzesenseBlazor 27 | 28 | Test project to bridge Wyze Sense to MQTT in a Blazor server app. Dipping my toes into some web dev. Still a work in progress 29 | 30 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace WyzeSenseBlazor.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | WyzeSenseBlazor 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | An error has occurred. This application may no longer respond until reloaded. 27 | 28 | 29 | An unhandled exception has occurred. See browser dev tools for details. 30 | 31 | Reload 32 | 🗙 33 |
34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataStorage/DataStoreService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Json; 4 | using Microsoft.Extensions.Logging; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace WyzeSenseBlazor.DataStorage 10 | { 11 | public class DataStoreService : IDataStoreService 12 | { 13 | private readonly IDataStoreOptions _dsOptions; 14 | private readonly ILogger _logger; 15 | 16 | public DataStore DataStore { get; } 17 | 18 | public DataStoreService(IDataStoreOptions dataStoreOptions, ILogger logger) 19 | { 20 | _dsOptions = dataStoreOptions; 21 | _logger = logger; 22 | 23 | if (File.Exists(dataStoreOptions.Path)) 24 | { 25 | DataStore = JsonSerializer.Deserialize(File.ReadAllText(dataStoreOptions.Path)); 26 | } 27 | else 28 | { 29 | DataStore = new(); 30 | _logger.LogDebug("Data Store file not found"); 31 | } 32 | } 33 | 34 | public async Task Save() 35 | { 36 | try 37 | { 38 | await File.WriteAllTextAsync(_dsOptions.Path, JsonSerializer.Serialize(DataStore)); 39 | } 40 | catch (Exception e) 41 | { 42 | _logger.LogError(e.Message); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .main > div { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model WyzeSenseBlazor.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/WyzeLogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace WyzeSenseBlazor 8 | { 9 | public class WyzeLogger : WyzeSenseCore.IWyzeSenseLogger 10 | { 11 | ILogger _logger; 12 | public WyzeLogger(ILogger Logger) 13 | { 14 | _logger = Logger; 15 | } 16 | public void Log(string level, string message) 17 | { 18 | switch(level) 19 | { 20 | case "Debug": _logger.LogDebug(message); break; 21 | case "Error": _logger.LogError(message); break; 22 | case "Trace": _logger.LogTrace(message); break; 23 | case "Warning": _logger.LogWarning(message); break; 24 | case "Info": 25 | case "Information": 26 | default: 27 | _logger.LogInformation(message); break; 28 | } 29 | } 30 | 31 | public void LogDebug(string message) 32 | { 33 | Log("Debug", message); 34 | } 35 | 36 | public void LogError(string message) 37 | { 38 | Log("Error", message); 39 | } 40 | 41 | public void LogInformation(string message) 42 | { 43 | Log("Information", message); 44 | } 45 | 46 | public void LogTrace(string message) 47 | { 48 | Log("Trace", message); 49 | } 50 | 51 | public void LogWarning(string message) 52 | { 53 | Log("Warning", message); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 36 |
37 | 38 | @code { 39 | private bool collapseNavMenu = true; 40 | 41 | private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 42 | 43 | private void ToggleNavMenu() 44 | { 45 | collapseNavMenu = !collapseNavMenu; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Pages/MQTTTemplates.razor: -------------------------------------------------------------------------------- 1 | @page "/MQTTTemplates" 2 | 3 | @using WyzeSenseBlazor.DataServices 4 | @using WyzeSenseCore 5 | @using AntDesign 6 | @inject IMQTTTemplateService templateService; 7 | 8 |

Templates

9 | 10 | 11 | 12 | 13 | 14 | @context.SensorType 15 | 16 | 17 |
18 | 19 | 39 | @code { 40 | int editSensorModelId; 41 | int editStateModelId; 42 | string newProperty; 43 | string newPropName; 44 | 45 | List templates; 46 | internal class Item 47 | { 48 | public string name { get; set; } 49 | public string value { get; set; } 50 | } 51 | List _properties = new List() 52 | { 53 | new Item { name = "Battery", value = "Battery"}, 54 | new Item { name = "Signal", value = "Signal"}, 55 | new Item { name = "State", value = "State"}, 56 | new Item { name = "Time", value = "Time"} 57 | 58 | }; 59 | 60 | public int pageSize { get; set; } = 10; 61 | 62 | protected override async Task OnInitializedAsync() 63 | { 64 | templates = new(); 65 | templates.AddRange(await templateService.GetTemplatesAsync()); 66 | 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /WyzeSenseCore/DataProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Channels; 7 | using System.Threading.Tasks; 8 | 9 | namespace WyzeSenseCore 10 | { 11 | internal class DataProcessor 12 | { 13 | // Fields and Properties 14 | protected CancellationToken CancelReads; 15 | protected CancellationToken CancelWrites; 16 | protected readonly Channel Messages; 17 | protected readonly Thread Thread; 18 | 19 | public DataProcessor( 20 | CancellationToken cancelReads = default(CancellationToken), 21 | CancellationToken cancelWrites = default(CancellationToken)) 22 | { 23 | // Initialize the channel 24 | this.CancelReads = cancelReads; 25 | this.CancelWrites = cancelWrites; 26 | this.Messages = Channel.CreateUnbounded(); 27 | 28 | Thread = new Thread(this.Dequeue); 29 | Thread.Start(); 30 | } 31 | 32 | 33 | public void Queue(Action WyzeAction) 34 | { 35 | if (!this.CancelWrites.IsCancellationRequested) 36 | this.Messages.Writer.TryWrite(WyzeAction); 37 | } 38 | 39 | private async void Dequeue() 40 | { 41 | while (!this.CancelReads.IsCancellationRequested) 42 | { 43 | try 44 | { 45 | var msg = await this.Messages.Reader.ReadAsync(this.CancelReads); 46 | msg?.Invoke(); 47 | } 48 | catch (OperationCanceledException oce) { } 49 | } 50 | } 51 | 52 | public void Shutdown() 53 | { 54 | this.CancelWrites = new CancellationToken(true); 55 | this.Messages.Reader.Completion.Wait(); 56 | this.CancelReads = new CancellationToken(true); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using WyzeSenseBlazor.Settings; 4 | using WyzeSenseBlazor.Mqtt.Options; 5 | using WyzeSenseBlazor.DataServices; 6 | using MQTTnet.Client.Options; 7 | using System; 8 | 9 | namespace WyzeSenseBlazor 10 | { 11 | public static class ServiceCollectionExtensions 12 | { 13 | public static IServiceCollection AddMqttClientHostedService(this IServiceCollection services) 14 | { 15 | services.AddMqttClientServiceWithConfig(aspOptionBuilder => 16 | { 17 | var clientSettings = AppSettingsProvider.ClientSettings; 18 | var brokerHostSettings = AppSettingsProvider.BrokerHostSettings; 19 | 20 | aspOptionBuilder 21 | .WithCredentials(clientSettings.UserName, clientSettings.Password) 22 | .WithClientId(clientSettings.Id) 23 | .WithTcpServer(brokerHostSettings.Host, brokerHostSettings.Port) 24 | .WithWillMessage(new MQTTnet.MqttApplicationMessageBuilder().WithTopic(AppSettingsProvider.ClientSettings.Topic).WithPayload("Offline").Build()); 25 | }); 26 | return services; 27 | } 28 | private static IServiceCollection AddMqttClientServiceWithConfig(this IServiceCollection services, Action configure) 29 | { 30 | services.AddSingleton(serviceProvider => 31 | { 32 | var optionBuilder = new AspCoreMqttClientOptionBuilder(serviceProvider); 33 | configure(optionBuilder); 34 | return optionBuilder.Build(); 35 | }); 36 | services.AddSingleton(); 37 | services.AddSingleton(serviceProvider => 38 | { 39 | return serviceProvider.GetService(); 40 | }); 41 | services.AddSingleton(serviceProvider => 42 | { 43 | var mqttClientService = serviceProvider.GetService(); 44 | var mqttClientServiceProvider = new MqttClientServiceProvider(mqttClientService); 45 | return mqttClientServiceProvider; 46 | }); 47 | return services; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /WyzeSense.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29911.84 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WyzeSenseCore", "WyzeSenseCore\WyzeSenseCore.csproj", "{A228CA12-990F-4956-855C-947834B846E4}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WyzeSenseApp", "WyzeSenseApp\WyzeSenseApp.csproj", "{68CAC7AA-47C6-40A2-81C5-CCA8C2EEF2FE}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WyzeSenseBlazor", "WyzeSenseBlazor\WyzeSenseBlazor.csproj", "{F39D8411-219F-4408-BB75-25F6F211B7B3}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WyzeSenseUpgrade", "WyzeSenseUpgrade\WyzeSenseUpgrade.csproj", "{80EF0641-0B40-4083-B290-3E0BEA8D8AA6}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {A228CA12-990F-4956-855C-947834B846E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {A228CA12-990F-4956-855C-947834B846E4}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {A228CA12-990F-4956-855C-947834B846E4}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {A228CA12-990F-4956-855C-947834B846E4}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {68CAC7AA-47C6-40A2-81C5-CCA8C2EEF2FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {68CAC7AA-47C6-40A2-81C5-CCA8C2EEF2FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {68CAC7AA-47C6-40A2-81C5-CCA8C2EEF2FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {68CAC7AA-47C6-40A2-81C5-CCA8C2EEF2FE}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {F39D8411-219F-4408-BB75-25F6F211B7B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {F39D8411-219F-4408-BB75-25F6F211B7B3}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {F39D8411-219F-4408-BB75-25F6F211B7B3}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {F39D8411-219F-4408-BB75-25F6F211B7B3}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {80EF0641-0B40-4083-B290-3E0BEA8D8AA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {80EF0641-0B40-4083-B290-3E0BEA8D8AA6}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {80EF0641-0B40-4083-B290-3E0BEA8D8AA6}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {80EF0641-0B40-4083-B290-3E0BEA8D8AA6}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {34BB0FDE-179F-41EC-9E0C-E1D67C0000A8} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /WyzeSenseApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Console; 6 | 7 | namespace WyzeSenseApp 8 | { 9 | class Program 10 | { 11 | private static WyzeSenseCore.IWyzeSenseLogger mylogger; 12 | static async Task Main(string[] args) 13 | { 14 | mylogger = new Logger(); 15 | // 16 | 17 | WyzeSenseCore.WyzeDongle dongle = new WyzeSenseCore.WyzeDongle(mylogger); 18 | dongle.OpenDevice(args[0]); 19 | dongle.OnSensorEvent += Dongle_OnSensorEvent; 20 | dongle.OnAddSensor += Dongle_OnAddSensor; 21 | dongle.OnRemoveSensor += Dongle_OnRemoveSensor; 22 | dongle.OnDongleStateChange += Dongle_OnDongleStateChange; 23 | Task processDongle = dongle.StartAsync(default(CancellationToken)); 24 | 25 | Random rand = new Random(); 26 | byte[] toB64 = new byte[8]; 27 | rand.NextBytes(toB64); 28 | 29 | bool done = false; 30 | while (!done) 31 | { 32 | string line = Console.ReadLine(); 33 | switch (line.Split(' ')[0]) 34 | { 35 | case "ledon": 36 | dongle.SetLedAsync(true); 37 | break; 38 | case "ledoff": 39 | dongle.SetLedAsync(false); 40 | break; 41 | case "list": 42 | dongle.RequestRefreshSensorListAsync(); 43 | break; 44 | case "scanon": 45 | dongle.StartScanAsync(); 46 | break; 47 | case "scanoff": 48 | dongle.StopScanAsync(); 49 | break; 50 | case "q": 51 | dongle.Stop(); 52 | done = true; 53 | break; 54 | case "del": 55 | if (line.Split(' ').Length < 2) break; 56 | string mac = line.Split(' ')[1]; 57 | if (mac.Length != 8) { mylogger.LogError("Invalid MAC"); break; } 58 | 59 | dongle.DeleteSensorAsync(mac); 60 | 61 | break; 62 | case "cc1310up": 63 | dongle.RequestCC1310Update(); 64 | break; 65 | default: 66 | mylogger.LogInformation("Type 'q' to exit"); 67 | break; 68 | } 69 | } 70 | await processDongle; 71 | 72 | } 73 | 74 | private static void Dongle_OnDongleStateChange(object sender, WyzeSenseCore.WyzeDongleState e) 75 | { 76 | mylogger.LogInformation($"Dongle Change: {e.ToString()}"); 77 | } 78 | 79 | private static void Dongle_OnRemoveSensor(object sender, WyzeSenseCore.WyzeSensor e) 80 | { 81 | mylogger.LogInformation($"Sensor removed {e.MAC}"); 82 | } 83 | 84 | private static void Dongle_OnAddSensor(object sender, WyzeSenseCore.WyzeSensor e) 85 | { 86 | mylogger.LogInformation($"Sensor added {e.MAC}"); 87 | } 88 | 89 | private static void Dongle_OnSensorEvent(object sender, WyzeSenseCore.WyzeSenseEvent e) 90 | { 91 | mylogger.LogInformation($"Alarm received {e.ToString()}"); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using System; 7 | using WyzeSenseBlazor.DataServices; 8 | using WyzeSenseBlazor.DataStorage; 9 | using WyzeSenseCore; 10 | using WyzeSenseBlazor.Settings; 11 | 12 | namespace WyzeSenseBlazor 13 | { 14 | public class Startup 15 | { 16 | public Startup(IConfiguration configuration) 17 | { 18 | Configuration = configuration; 19 | MapConfiguration(); 20 | } 21 | public IConfiguration Configuration { get; } 22 | 23 | private void MapConfiguration() 24 | { 25 | BrokerHostSettings brokerHostSettings = new(); 26 | Configuration.GetSection(nameof(BrokerHostSettings)).Bind(brokerHostSettings); 27 | AppSettingsProvider.BrokerHostSettings = brokerHostSettings; 28 | 29 | ClientSettings clientSettings = new(); 30 | Configuration.GetSection(nameof(ClientSettings)).Bind(clientSettings); 31 | AppSettingsProvider.ClientSettings = clientSettings; 32 | 33 | WyzeSettings wyzeSettings = new(); 34 | Configuration.GetSection(nameof(WyzeSettings)).Bind(wyzeSettings); 35 | AppSettingsProvider.WyzeSettings = wyzeSettings; 36 | Console.WriteLine("Dongle path: " + wyzeSettings.UsbPath); 37 | 38 | DatabaseSettings databaseSettings = new(); 39 | Configuration.GetSection(nameof(DatabaseSettings)).Bind(databaseSettings); 40 | AppSettingsProvider.DatabaseSettings = databaseSettings; 41 | } 42 | 43 | 44 | // This method gets called by the runtime. Use this method to add services to the container. 45 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 46 | public void ConfigureServices(IServiceCollection services) 47 | { 48 | services.AddRazorPages(); 49 | services.AddServerSideBlazor(); 50 | services.AddAntDesign(); 51 | services.AddMqttClientHostedService(); 52 | 53 | 54 | services.AddSingleton(new DataStoreOptions() 55 | { 56 | Path = AppSettingsProvider.DatabaseSettings.DatabasePath 57 | }); 58 | services.AddSingleton(); 59 | 60 | services.AddSingleton(); 61 | services.AddSingleton(); 62 | services.AddHostedService(); 63 | services.AddSingleton(); 64 | 65 | services.AddScoped(); 66 | } 67 | 68 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 69 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 70 | { 71 | if (env.IsDevelopment()) 72 | { 73 | app.UseDeveloperExceptionPage(); 74 | } 75 | else 76 | { 77 | app.UseExceptionHandler("/Error"); 78 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 79 | app.UseHsts(); 80 | } 81 | 82 | app.UseHttpsRedirection(); 83 | app.UseStaticFiles(); 84 | 85 | app.UseRouting(); 86 | 87 | app.UseEndpoints(endpoints => 88 | { 89 | endpoints.MapBlazorHub(); 90 | endpoints.MapFallbackToPage("/_Host"); 91 | }); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /WyzeSenseCore/ByteBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace WyzeSenseCore 6 | { 7 | internal class ByteBuffer 8 | { 9 | private Memory _buffer; 10 | 11 | //This is the first byte in the buffer 12 | private int head; 13 | 14 | //This is the first byte AFTER the buffer data 15 | private int tail; 16 | 17 | //max number of bytes the buffer can hold 18 | private int capacity; 19 | 20 | //Max number of bytes the buffer can grow to 21 | private int maxCapacity; 22 | 23 | //Number of bytes contained in the buffer 24 | public int Size 25 | { 26 | get 27 | { 28 | if (IsEmpty) 29 | return 0; 30 | else if (tail > head) 31 | return tail - head; 32 | else 33 | return (capacity - head) + tail; 34 | } 35 | } 36 | 37 | public bool IsEmpty 38 | { 39 | get 40 | { 41 | return (tail == head); 42 | } 43 | } 44 | 45 | private Action Logger; 46 | 47 | /// 48 | /// Create a new ByteBuffer 49 | /// 50 | /// Starting capacity of data buffer 51 | /// Max the buffer can auto grow to 52 | public ByteBuffer(int Capacity, Action Logger = null, int MaxCapacity = 4096 * 4) 53 | { 54 | head = 0; 55 | tail = 0; 56 | this.capacity = Capacity; 57 | this.maxCapacity = MaxCapacity; 58 | this._buffer = new Memory(new byte[this.capacity]); 59 | this.Logger = Logger; 60 | 61 | } 62 | 63 | public void Queue(Span Data) 64 | { 65 | int dataLength = Data.Length; 66 | 67 | while (Size + dataLength >= capacity) 68 | { 69 | Grow(); 70 | } 71 | 72 | int overflowBytes = (tail + dataLength) - capacity; 73 | 74 | if (overflowBytes > 0) 75 | { 76 | int bytesAtEnd = dataLength - overflowBytes; 77 | 78 | Log(string.Format("Queue - Wrap - Data.Length:{0,5}", dataLength)); 79 | 80 | Data.Slice(0, bytesAtEnd).CopyTo(_buffer.Slice(tail).Span); 81 | Data.Slice(bytesAtEnd).CopyTo(_buffer.Slice(0, overflowBytes).Span); 82 | 83 | } 84 | else 85 | { 86 | Log(string.Format("Queue - NoWr - Data.Length:{0,5}, buffer Size: {1}", dataLength, _buffer.Slice(tail).Span.Length)); 87 | Data.CopyTo(_buffer.Slice(tail).Span); 88 | 89 | } 90 | //if (IsEmpty) 91 | // head += 1; 92 | //Progess the Tail 93 | tail = (tail + dataLength) % capacity; 94 | 95 | } 96 | 97 | public bool Peek(Span Data) => Dequeue(Data, true); 98 | public bool Dequeue(Span Data) => Dequeue(Data, false); 99 | private bool Dequeue(Span Data, bool Peek = false) 100 | { 101 | int bytesToGet = Data.Length; 102 | 103 | if (bytesToGet > Size) 104 | { 105 | Log($"Dequeue - Cannot retrieve {bytesToGet} bytes from {Size} bytes"); 106 | return false; 107 | } 108 | 109 | int overflowBytes = (head + bytesToGet) - capacity; 110 | if (overflowBytes > 0) 111 | { 112 | int bytesAtEnd = bytesToGet - overflowBytes; 113 | Log(string.Format("{1} - Wrap - Retrieve:{0:5}", bytesToGet, Peek ? "Peek" : "Dequeue")); 114 | _buffer.Slice(head, bytesAtEnd).Span.CopyTo(Data); 115 | _buffer.Slice(0, overflowBytes).Span.CopyTo(Data.Slice(bytesAtEnd)); 116 | 117 | } 118 | else 119 | { 120 | Log(string.Format("{1} - NoWr - Retrieve:{0,5}", bytesToGet, Peek ? "Peek" : "Dequeue")); 121 | _buffer.Slice(head, bytesToGet).Span.CopyTo(Data); 122 | } 123 | 124 | //Progress the head 125 | if (!Peek) 126 | head = (head + bytesToGet) % capacity; 127 | 128 | return true; 129 | } 130 | public bool Burn(uint Count) 131 | { 132 | if(Count > Size) 133 | { 134 | Log($"Burn - Cannot burn {Count} bytes from {Size} bytes"); 135 | return false; 136 | } 137 | head = (int)((head + Count) % capacity); 138 | return true; 139 | } 140 | 141 | private void Grow() 142 | { 143 | if (capacity * 2 > maxCapacity) 144 | throw new OutOfMemoryException("Cannot grow past Max Capacity of " + maxCapacity); 145 | Log($"Grow - Pre"); 146 | 147 | Memory newBuffer = new Memory(new byte[capacity * 2]); 148 | if (IsEmpty) 149 | _buffer = newBuffer; 150 | else if (tail > head) 151 | { 152 | _buffer.Slice(head, Size).CopyTo(newBuffer); 153 | tail = Size; 154 | head = 0; 155 | _buffer = newBuffer; 156 | } 157 | else 158 | { 159 | //Wraps 160 | int bytesAtEnd = capacity - head; 161 | _buffer.Slice(head, bytesAtEnd).CopyTo(newBuffer); 162 | _buffer.Slice(0, tail).CopyTo(newBuffer.Slice(bytesAtEnd)); 163 | tail = Size; 164 | head = 0; 165 | _buffer = newBuffer; 166 | } 167 | capacity *= 2; 168 | Log($"Grow - Post"); 169 | } 170 | private void Log(string message, bool stamp = true) 171 | { 172 | if (stamp) 173 | message = string.Format("[ByteBuffer] C:{0,5} S:{1,5}, H:{2,5} T:{3,5} - {4}", capacity, Size, head, tail, message); 174 | else 175 | message = "[ByteBuffer] " + message; 176 | Logger?.Invoke(message); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/Pages/WyzeSensors.razor: -------------------------------------------------------------------------------- 1 | @page "/wyzesensors" 2 | 3 | @using WyzeSenseBlazor.DataServices 4 | @using WyzeSenseCore 5 | @using AntDesign 6 | @using System.Diagnostics 7 | @using WyzeSenseBlazor.DataStorage.Models 8 | @inject IWyzeSenseService SensorService 9 | @inject DrawerService DrawerService 10 | 11 |

Wyze

12 | @if (dongle == null) 13 | { 14 | 15 | } 16 | else 17 | { 18 | 19 | @dongle.MAC 20 | 21 | @if (dongle.IsInclusive) 22 | { 23 | 24 | } 25 | else if (dongle.LEDState) 26 | { 27 | 28 | } 29 | else 30 | { 31 | 32 | } 33 | 34 | @dongle.DeviceType 35 | @dongle.AuthState 36 | @dongle.Version 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | @if (!dongle.IsInclusive) 49 | { 50 | 51 | } 52 | else 53 | { 54 | 55 | } 56 | 57 | 58 | } 59 | 60 | @if (sensorCollection == null) 61 | { 62 | 63 | } 64 | else 65 | { 66 | 67 | 68 | 69 | 70 | @if (sensorContext.LastActive == DateTime.MinValue) 71 | { 72 | - 73 | } 74 | else 75 | { 76 | 77 | } 78 | 79 | 80 | 81 |
88 | } 89 | 90 | @code { 91 | private Dictionary columnNumber = new Dictionary { 92 | { "xxl", 3 }, 93 | { "xl", 3}, 94 | { "lg", 2}, 95 | { "md", 2}, 96 | { "sm", 1}, 97 | { "xs", 1} 98 | }; 99 | 100 | private Dictionary sensorCollection; 101 | private WyzeDongleState dongle; 102 | string mac = "AABBCCDD"; 103 | 104 | protected override async Task OnInitializedAsync() 105 | { 106 | sensorCollection = new(); 107 | 108 | dongle = SensorService.GetDongleState(); 109 | this.SensorService.OnEvent += OnSensorAlarm; 110 | this.SensorService.OnDongleStateChange += OnDongleStateChange; 111 | SensorService.OnAddSensor += OnAddSensor; 112 | SensorService.OnRemoveSensor += OnRemoveSensor; 113 | 114 | var sensors = await SensorService.GetSensorAsync(); 115 | foreach (var sensor in sensors) 116 | { 117 | sensorCollection.TryAdd(sensor.MAC, sensor); 118 | 119 | } 120 | Console.WriteLine("Testing"); 121 | 122 | } 123 | private async void OnRemoveSensor(object sender, string sensorMAC) 124 | { 125 | sensorCollection.Remove(sensorMAC); 126 | await InvokeAsync(this.StateHasChanged); 127 | } 128 | private async void OnAddSensor(object sender, WyzeSensorModel sensor) 129 | { 130 | sensorCollection.Add(sensor.MAC, sensor); 131 | await InvokeAsync(this.StateHasChanged); 132 | } 133 | private async void OnSensorAlarm(object sender, WyzeSenseEvent wyzeEvent) 134 | { 135 | await InvokeAsync(this.StateHasChanged); 136 | } 137 | 138 | private async void SensorDelete(string MAC) 139 | { 140 | 141 | Console.Write("Teststsett"); 142 | //await SensorService.RequestDeleteSensor(MAC); 143 | } 144 | 145 | private async void OnDongleStateChange(object sender, WyzeDongleState state) 146 | { 147 | Console.WriteLine("Received dongle state:" + state); 148 | dongle = state; 149 | await InvokeAsync(this.StateHasChanged); 150 | } 151 | private void OnLEDSwitchChange(bool newState) 152 | { 153 | Console.WriteLine("New state:" + newState); 154 | } 155 | 156 | private async Task OpenDrawer(string MAC) 157 | { 158 | var options = new DrawerOptions() 159 | { 160 | Title = "Edit Sensor", 161 | Width = 350 162 | }; 163 | Console.WriteLine("Test"); 164 | var result = await DrawerService.CreateAsync(options, sensorCollection[MAC]); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataServices/WyzeSenseService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using WyzeSenseCore; 8 | using WyzeSenseBlazor.Settings; 9 | using WyzeSenseBlazor.DataStorage; 10 | using WyzeSenseBlazor.DataStorage.Models; 11 | using System.Collections.Generic; 12 | 13 | namespace WyzeSenseBlazor.DataServices 14 | { 15 | public class WyzeSenseService : IHostedService, IWyzeSenseService 16 | { 17 | private readonly ILogger _logger; 18 | private readonly WyzeSenseCore.IWyzeDongle _wyzeDongle; 19 | private readonly IDataStoreService _dataStore; 20 | 21 | private string devicePath; 22 | 23 | private Task processTask; 24 | 25 | private WyzeDongleState dongleState; 26 | public WyzeSenseService(ILogger logger, WyzeSenseCore.IWyzeDongle wyzeDongle, IDataStoreService dataStore) 27 | { 28 | _logger = logger; 29 | _logger.LogInformation($"Creating WyzeSenseService"); 30 | 31 | _dataStore = dataStore; 32 | _wyzeDongle = wyzeDongle; 33 | 34 | devicePath = AppSettingsProvider.WyzeSettings.UsbPath; 35 | 36 | _wyzeDongle.OnAddSensor += _wyzeDongle_OnAddSensor; 37 | _wyzeDongle.OnRemoveSensor += _wyzeDongle_OnRemoveSensor; 38 | _wyzeDongle.OnDongleStateChange += _wyzeDongle_OnDongleStateChange; 39 | _wyzeDongle.OnSensorEvent += _wyzeDongle_OnSensorEvent; 40 | } 41 | 42 | 43 | 44 | public event EventHandler OnAddSensor; 45 | public event EventHandler OnRemoveSensor; 46 | public event EventHandler OnEvent; 47 | public event EventHandler OnDongleStateChange; 48 | public event EventHandler OnFailStart; 49 | 50 | public bool Running { get=> running; } 51 | private bool running = false; 52 | public async Task StartAsync(CancellationToken cancellationToken) 53 | { 54 | 55 | //Let us try to start 56 | if (!_wyzeDongle.OpenDevice(devicePath)) 57 | { 58 | OnFailStart?.Invoke(this, $"Failed to open device: {devicePath}"); 59 | } 60 | 61 | //At this point we should have succesfully opened. 62 | running = true; 63 | _logger.LogInformation("[ExecuteAsync] Starting dongle start"); 64 | await _wyzeDongle.StartAsync(cancellationToken); 65 | _logger.LogInformation("[ExecuteAsync] Finished dongle start"); 66 | running = false; 67 | 68 | } 69 | 70 | public Task StopAsync(CancellationToken cancellationToken) 71 | { 72 | _wyzeDongle.Stop(); 73 | return Task.CompletedTask; 74 | } 75 | 76 | private void _wyzeDongle_OnDongleStateChange(object sender, WyzeDongleState e) 77 | { 78 | dongleState = e; 79 | this.OnDongleStateChange?.Invoke(this, e); 80 | } 81 | private async void _wyzeDongle_OnSensorEvent(object sender, WyzeSenseEvent e) 82 | { 83 | var dbSensor = await GetOrCreateSensor(e.Sensor); 84 | dbSensor.LastActive = DateTime.Now; 85 | OnEvent?.Invoke(this, e); 86 | } 87 | 88 | 89 | private async void _wyzeDongle_OnRemoveSensor(object sender, WyzeSensor e) 90 | { 91 | if(_dataStore.DataStore.Sensors.Remove(e.MAC)) 92 | { 93 | OnRemoveSensor?.Invoke(this, e.MAC); 94 | await _dataStore.Save(); 95 | } 96 | } 97 | 98 | private async void _wyzeDongle_OnAddSensor(object sender, WyzeSensor e) 99 | { 100 | //TODO: Generate sub sensors for climate and keypad. 101 | var sensorModel = await GetOrCreateSensor(e); 102 | OnAddSensor?.Invoke(this, sensorModel); 103 | } 104 | private async Task GetOrCreateSensor(WyzeSensor Sensor) 105 | { 106 | if(_dataStore.DataStore.Sensors.TryGetValue(Sensor.MAC, out var existSensor)) 107 | { 108 | return existSensor; 109 | } 110 | 111 | var sensorModel = new WyzeSensorModel() 112 | { 113 | Alias = "", 114 | Description = "", 115 | MAC = Sensor.MAC, 116 | SensorType = (int)Sensor.Type, 117 | LastActive = DateTime.Now 118 | }; 119 | _dataStore.DataStore.Sensors.TryAdd(sensorModel.MAC, sensorModel); 120 | await _dataStore.Save(); 121 | return sensorModel; 122 | } 123 | 124 | 125 | public void RequestRefreshSensorListAsync() 126 | { 127 | _wyzeDongle.RequestRefreshSensorListAsync(); 128 | } 129 | 130 | public void SetLEDOff() 131 | { 132 | _wyzeDongle.SetLedAsync(false); 133 | } 134 | 135 | public void SetLEDOn() 136 | { 137 | _wyzeDongle.SetLedAsync(true); 138 | } 139 | 140 | public async Task StartScanAsync(int Timeout = 60 * 1000) 141 | { 142 | _logger.LogTrace("[StartScanAsync] Called"); 143 | await _wyzeDongle.StartScanAsync(Timeout); 144 | } 145 | 146 | public void Stop() 147 | { 148 | _wyzeDongle.Stop(); 149 | } 150 | 151 | public async Task StopScanAsync() 152 | { 153 | _logger.LogTrace("[StartScanAsync] Called"); 154 | await _wyzeDongle.StopScanAsync(); 155 | } 156 | 157 | public async Task GetSensorAsync() 158 | { 159 | return _dataStore.DataStore.Sensors.Values.ToArray(); 160 | } 161 | 162 | public async Task RequestDeleteSensor(string MAC) 163 | { 164 | await _wyzeDongle.DeleteSensorAsync(MAC); 165 | } 166 | 167 | 168 | public WyzeDongleState GetDongleState() 169 | { 170 | return _wyzeDongle.GetDongleState(); 171 | } 172 | 173 | public async Task SetAlias(string MAC, string Alias) 174 | { 175 | if (_dataStore.DataStore.Sensors.TryGetValue(MAC, out var sensor)) 176 | { 177 | sensor.Alias = Alias; 178 | await _dataStore.Save(); 179 | } 180 | } 181 | 182 | public async Task SetDescription(string MAC, string Description) 183 | { 184 | if (_dataStore.DataStore.Sensors.TryGetValue(MAC, out var sensor)) 185 | { 186 | sensor.Description = Description; 187 | await _dataStore.Save(); 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /WyzeSenseBlazor/DataServices/MqttClientService.cs: -------------------------------------------------------------------------------- 1 | using MQTTnet; 2 | using MQTTnet.Client; 3 | using MQTTnet.Client.Connecting; 4 | using MQTTnet.Client.Disconnecting; 5 | using MQTTnet.Client.Options; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Text.Json; 9 | using WyzeSenseBlazor.Settings; 10 | using WyzeSenseBlazor.DataStorage; 11 | using WyzeSenseBlazor.DataStorage.Models; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | 15 | namespace WyzeSenseBlazor.DataServices 16 | { 17 | public class MqttClientService : IMqttClientService 18 | { 19 | private readonly IWyzeSenseService _wyzeSenseService; 20 | private readonly IDataStoreService _dataStore; 21 | private readonly IMqttClient _mqttClient; 22 | private readonly IMqttClientOptions _options; 23 | 24 | public MqttClientService(IMqttClientOptions options, IWyzeSenseService wyzeSenseService, IDataStoreService dataStore) 25 | { 26 | _options = options; 27 | 28 | _dataStore = dataStore; 29 | 30 | _wyzeSenseService = wyzeSenseService; 31 | _wyzeSenseService.OnEvent += _wyzeSenseService_OnEvent; 32 | 33 | _mqttClient = new MqttFactory().CreateMqttClient(); 34 | ConfigureMqttClient(); 35 | } 36 | 37 | private void _wyzeSenseService_OnEvent(object sender, WyzeSenseCore.WyzeSenseEvent e) 38 | { 39 | e.Data.Add("timestamp", e.ServerTime.ToString()); 40 | 41 | bool hasPublished = false; 42 | 43 | //Topic should always start with the root topic. 44 | string topic = AppSettingsProvider.ClientSettings.Topic; 45 | 46 | if (_dataStore.DataStore.Sensors.TryGetValue(e.Sensor.MAC, out var sensor)) 47 | { 48 | List toRemove = new(); 49 | if (sensor.Topics?.Count() > 0) 50 | { 51 | foreach (var topicTemplate in sensor.Topics) 52 | { 53 | if (_dataStore.DataStore.Templates.TryGetValue(topicTemplate.TemplateName, out var template)) 54 | { 55 | Dictionary payloadData = new(); 56 | 57 | //Template does exist, need to publish a message for each package. 58 | foreach (var payloadPack in template.PayloadPackages) 59 | { 60 | payloadData.Clear(); 61 | 62 | topic = string.Join('/', topic, sensor.Alias, topicTemplate.RootTopic, payloadPack.Topic); 63 | //Replace double slash to accomodate blank root topic. 64 | topic = System.Text.RegularExpressions.Regex.Replace(topic, @"/+", @"/"); 65 | //Remove trailing slash to accomdate blank payload topic. 66 | topic = topic.TrimEnd('/'); 67 | 68 | foreach (var pair in payloadPack.Payload) 69 | { 70 | if (e.Data.TryGetValue(pair.Value, out var value)) 71 | payloadData.Add(pair.Key, value); 72 | } 73 | if (payloadData.Count() > 0) 74 | { 75 | //If event data contained any of the payload packet data lets add time and publish. 76 | payloadData.Add("timestamp", e.ServerTime.ToString()); 77 | PublishMessageAsync(topic, JsonSerializer.Serialize(payloadData)); 78 | hasPublished = true; 79 | } 80 | } 81 | } 82 | else 83 | { 84 | //Template doesn't exist 85 | toRemove.Add(topicTemplate); 86 | } 87 | } 88 | //Remove the topic templates that didn't have a valid template associated. 89 | if(toRemove.Count() > 0) 90 | toRemove.ForEach(p => sensor.Topics.Remove(p)); 91 | } 92 | else if(sensor.Alias.Length > 0) 93 | { 94 | //Sensor with no topics, publish with alias. 95 | PublishMessageAsync(string.Join('/', topic, sensor.Alias), JsonSerializer.Serialize(e.Data)); 96 | hasPublished = true; 97 | } 98 | } 99 | if (!hasPublished) 100 | { 101 | //No database sensor, publish all data to MAC 102 | PublishMessageAsync(string.Join('/', topic, e.Sensor.MAC), JsonSerializer.Serialize(e.Data)); 103 | } 104 | 105 | } 106 | 107 | private async Task PublishMessageAsync(string topic, string payload) 108 | { 109 | var message = new MqttApplicationMessageBuilder() 110 | .WithTopic(topic) 111 | .WithPayload(payload) 112 | .WithExactlyOnceQoS() 113 | .Build(); 114 | await _mqttClient.PublishAsync(message); 115 | } 116 | 117 | 118 | private void ConfigureMqttClient() 119 | { 120 | _mqttClient.ConnectedHandler = this; 121 | _mqttClient.DisconnectedHandler = this; 122 | } 123 | 124 | public async Task HandleConnectedAsync(MqttClientConnectedEventArgs eventArgs) 125 | { 126 | await _mqttClient.PublishAsync(new MqttApplicationMessageBuilder() 127 | .WithTopic(AppSettingsProvider.ClientSettings.Topic) 128 | .WithPayload("Online") 129 | .WithExactlyOnceQoS() 130 | .Build()); 131 | } 132 | 133 | public Task HandleDisconnectedAsync(MqttClientDisconnectedEventArgs eventArgs) 134 | { 135 | //throw new System.NotImplementedException(); 136 | return Task.CompletedTask; 137 | } 138 | 139 | public async Task StartAsync(CancellationToken cancellationToken) 140 | { 141 | await _mqttClient.ConnectAsync(_options); 142 | if (!_mqttClient.IsConnected) 143 | { 144 | await _mqttClient.ReconnectAsync(); 145 | } 146 | System.Console.WriteLine("Finishing starting MQTT service"); 147 | } 148 | 149 | public async Task StopAsync(CancellationToken cancellationToken) 150 | { 151 | if (cancellationToken.IsCancellationRequested) 152 | { 153 | var disconnectOption = new MqttClientDisconnectOptions 154 | { 155 | ReasonCode = MqttClientDisconnectReason.NormalDisconnection, 156 | ReasonString = "NormalDiconnection" 157 | }; 158 | await _mqttClient.DisconnectAsync(disconnectOption, cancellationToken); 159 | } 160 | await _mqttClient.DisconnectAsync(); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /WyzeSenseBlazor/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /WyzeSenseCore/Packet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Security.Cryptography; 8 | using System.Text; 9 | 10 | namespace WyzeSenseCore 11 | { 12 | internal class Command 13 | { 14 | public enum CommandTypes : byte 15 | { 16 | TYPE_SYNC = 0x43, 17 | TYPE_ASYNC = 0x53 18 | } 19 | public enum CommandIDs: byte 20 | { 21 | RequestEnr = 0x02, 22 | RequestEnrResp = 0x03, 23 | RequestMAC = 0x04, 24 | RequestMACResp = 0x05, 25 | RequestKey = 0x06, 26 | SetCh554Update = 0x0E, 27 | UpdateCC1310 = 0x12, 28 | UpdateCC1310Resp = 0x13, 29 | FinishAuth = 0x14, 30 | AuthResp = 0x15, 31 | RequestDongleVersion = 0x16, 32 | RequestDongleVersionResp = 0x17, 33 | NotifySensorAlarm = 0x19, 34 | StartStopScan = 0x1C, 35 | StartStopScanResp = 0x1D, 36 | NotifySensorStart = 0x20, 37 | SetSensorRandomDate = 0x21, 38 | SensorRandomDateResp = 0x22, 39 | VerifySensor = 0x23, 40 | VerifySensorResp = 0x24, 41 | DeleteSensor = 0x25, 42 | DeleteSensorResp = 0x26, 43 | GetDeviceType = 0x27, 44 | GetDeviceTypeResp = 0x28, 45 | GetSensorCount = 0x2E, 46 | GetSensorCountResp = 0x2F, 47 | GetSensorList = 0x30, 48 | GetSensorListResp = 0x31, 49 | RequestSyncTime = 0x32, 50 | NotifyEventLog = 0x35, 51 | Unk1 = 0x37,//Is this missed alarms? 52 | SetLED = 0x3d, 53 | SetLEDResp = 0x3e, 54 | SendKeypadEvent = 0x53, 55 | KeyPadEvent = 0x55, 56 | 57 | Ack = 0xFF 58 | //HMS 59 | /* 60 | GetBatteryVolt = 0x57 61 | 62 | */ 63 | } 64 | 65 | public readonly byte CommandType; 66 | public readonly byte CommandID; 67 | 68 | private Command(CommandTypes CommandType, CommandIDs CommandID) 69 | { 70 | this.CommandType = (byte)CommandType; 71 | this.CommandID = (byte)CommandID; 72 | } 73 | public static Command CMD_SEND_KP_EVENT => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.SendKeypadEvent); 74 | public static Command CMD_GET_ENR => new Command(CommandTypes.TYPE_SYNC, CommandIDs.RequestEnr); 75 | public static Command CMD_GET_MAC => new Command(CommandTypes.TYPE_SYNC, CommandIDs.RequestMAC); 76 | public static Command CMD_GET_KEY => new Command(CommandTypes.TYPE_SYNC, CommandIDs.RequestKey); 77 | public static Command CMD_GET_DEVICE_TYPE => new Command(CommandTypes.TYPE_SYNC, CommandIDs.GetDeviceType); 78 | public static Command CMD_UPDATE_CC1310 => new Command(CommandTypes.TYPE_SYNC, CommandIDs.UpdateCC1310); 79 | public static Command CMD_SET_CH554_UPGRADE => new Command(CommandTypes.TYPE_SYNC, CommandIDs.SetCh554Update); 80 | 81 | public static Command ASYNC_ACK => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.Ack); 82 | 83 | 84 | public static Command CMD_FINISH_AUTH => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.FinishAuth); 85 | public static Command CMD_GET_DONGLE_VERSION => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.RequestDongleVersion); 86 | public static Command CMD_START_STOP_SCAN => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.StartStopScan); 87 | public static Command CMD_SET_SENSOR_RANDOM_DATE => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.SetSensorRandomDate); 88 | public static Command CMD_VERIFY_SENSOR => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.VerifySensor); 89 | public static Command CMD_DEL_SENSOR => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.DeleteSensor); 90 | public static Command CMD_GET_SENSOR_COUNT => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.GetSensorCount); 91 | public static Command CMD_GET_SENSOR_LIST => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.GetSensorList); 92 | 93 | 94 | public static Command NOTIFY_SENSOR_ALARM => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.NotifySensorAlarm); 95 | public static Command NOTIFY_SENSOR_START => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.NotifySensorStart); 96 | public static Command NOTIFY_SYNC_TIME => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.RequestSyncTime); 97 | public static Command NOTIFY_EVENT_LOG => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.NotifyEventLog); 98 | public static Command CMD_SET_LIGHT => new Command(CommandTypes.TYPE_ASYNC, CommandIDs.SetLED); 99 | } 100 | internal class BasePacket 101 | { 102 | public Command PktCommand { get; set; } 103 | public byte Length { get; set; } 104 | public BasePacket(Command Cmd) 105 | { 106 | PktCommand = Cmd; 107 | } 108 | public virtual byte[] Write() 109 | { 110 | //This will append the header and the checksum 111 | using(BinaryWriter writer = new BinaryWriter(new MemoryStream())) 112 | { 113 | byte[] payload = Encode(); 114 | //Write the header. 115 | writer.Write((ushort)0x55AA); 116 | writer.Write((byte)PktCommand.CommandType); 117 | 118 | Length = (byte)((payload == null ? 0 : payload.Length) + 3);//Add three to include checksum length + cmd ID 119 | writer.Write((byte)Length); 120 | 121 | writer.Write((byte)PktCommand.CommandID); 122 | 123 | //Write the payload 124 | if(payload != null) 125 | writer.Write(payload); 126 | 127 | //Write the checksum 128 | writer.BaseStream.Flush(); 129 | writer.Write(GetChecksum((writer.BaseStream as MemoryStream).ToArray())); 130 | 131 | writer.BaseStream.Flush(); 132 | return (writer.BaseStream as MemoryStream).ToArray(); 133 | } 134 | } 135 | public virtual byte[] Encode() { return null; } 136 | public virtual void Decode() { throw new NotImplementedException(); } 137 | 138 | internal ushort GetChecksum(byte[] data) 139 | { 140 | ushort res = (ushort)data.Sum(x => x); 141 | return (ushort)((ushort)((res & 0xff) << 8) | ((res >> 8) & 0xff)); 142 | } 143 | 144 | public static BasePacket RequestEnableScan() => new BytePacket(Command.CMD_START_STOP_SCAN, 0x01); 145 | public static BasePacket RequestDisableScan() => new BytePacket(Command.CMD_START_STOP_SCAN, 0x00); 146 | public static BasePacket RequestSensorList(byte Amount) => new BytePacket(Command.CMD_GET_SENSOR_LIST, Amount); 147 | public static BasePacket FinishAuth() => new BytePacket(Command.CMD_FINISH_AUTH, 0xff); 148 | public static BasePacket AckPacket(byte CmdToAck) => new AckPacket(CmdToAck); 149 | public static BasePacket RequestDeviceType() => new BasePacket(Command.CMD_GET_DEVICE_TYPE); 150 | public static BasePacket RequestVersion() => new BasePacket(Command.CMD_GET_DONGLE_VERSION); 151 | public static BasePacket RequestMAC() => new BasePacket(Command.CMD_GET_MAC); 152 | public static BasePacket RequestKey() => new BasePacket(Command.CMD_GET_KEY); 153 | public static BasePacket RequestSensorCount() => new BasePacket(Command.CMD_GET_SENSOR_COUNT); 154 | public static BasePacket UpdateCC1310() => new BasePacket(Command.CMD_UPDATE_CC1310); 155 | public static BasePacket UpdateCH554() => new BasePacket(Command.CMD_SET_CH554_UPGRADE); 156 | public static BasePacket SetSensorRandomDate(string MAC) 157 | { 158 | Random random = new Random((int)DateTime.Now.Ticks); 159 | //dongle_app generates a random string of the following characters. 160 | const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 161 | 162 | for (int i = 0; i < 16; i++) 163 | MAC += chars[random.Next(chars.Length)]; 164 | 165 | return new ByteArrayPacket(Command.CMD_SET_SENSOR_RANDOM_DATE, ASCIIEncoding.ASCII.GetBytes(MAC)); 166 | } 167 | public static BasePacket RequestEnr(byte[] RValue) 168 | { 169 | return new ByteArrayPacket(Command.CMD_GET_ENR, RValue); ; 170 | } 171 | public static BasePacket DeleteSensor(string MAC) 172 | { 173 | return new ByteArrayPacket(Command.CMD_DEL_SENSOR, ASCIIEncoding.ASCII.GetBytes(MAC)); 174 | } 175 | public static BasePacket VerifySensor(string MAC) 176 | { 177 | byte[] buffer = new byte[10]; 178 | Array.Copy(ASCIIEncoding.ASCII.GetBytes(MAC), buffer, 8); 179 | buffer[8] = 0xff; 180 | buffer[9] = 0x04; 181 | 182 | return new ByteArrayPacket(Command.CMD_VERIFY_SENSOR, buffer); 183 | } 184 | public static BasePacket SyncTimeAck() => new ByteArrayPacket(Command.NOTIFY_SYNC_TIME, BitConverter.GetBytes(DateTime.UtcNow.Ticks)); 185 | public static BasePacket SetLightOn() => new BytePacket(Command.CMD_SET_LIGHT, 0xff); 186 | public static BasePacket SetLightOff() => new BytePacket(Command.CMD_SET_LIGHT, 0); 187 | } 188 | internal class KeyPadEventPacket : BasePacket 189 | { 190 | public byte Status { get; set; } 191 | public KeyPadEventPacket(Command Cmd, byte KeyPadStatus) : base(Cmd) 192 | { 193 | Status = KeyPadStatus; 194 | } 195 | public override byte[] Encode() 196 | { 197 | var mybuf = new byte[0x13] 198 | { 199 | 0xAA,//hdr 200 | 0x55,//hdr 201 | 0x53, //Pkt type 202 | 0xf, //len 203 | 0x53,//Cmd Id 204 | 0,//43 205 | 0,//42 206 | 0,//41 207 | 0,//40 208 | 0,//3F 209 | 0,//3E 210 | 0,//3D 211 | 0,//3C - going to be lsbyte of *&(param1+5)//unknown 212 | 0xE9,//3B 213 | 0x02,//3A 214 | 0x09,//39 215 | 0,//38 - State code? 1=disarm; 2=activehome; 3=activeaway; 4=inactive; 5=? 216 | 0,//37 checksum 217 | 0 //36 checksum 218 | }; 219 | 220 | return base.Encode(); 221 | } 222 | } 223 | internal class BytePacket : BasePacket 224 | { 225 | public byte Value { get; set; } 226 | public BytePacket(Command Cmd, byte Value) : base(Cmd) 227 | { 228 | this.Value = Value; 229 | } 230 | public override byte[] Encode() 231 | { 232 | return new byte[1] { Value }; 233 | } 234 | } 235 | internal class ByteArrayPacket : BasePacket 236 | { 237 | public byte [] Value { get; set; } 238 | public ByteArrayPacket(Command Cmd, byte [] Value) : base(Cmd) 239 | { 240 | this.Value = Value; 241 | } 242 | public override byte[] Encode() 243 | { 244 | return Value; 245 | } 246 | } 247 | internal class AckPacket : BasePacket 248 | { 249 | public byte ToAcknowledge { get; set; } 250 | public AckPacket(byte CmdToAck) : base(Command.ASYNC_ACK) 251 | { 252 | ToAcknowledge = CmdToAck; 253 | } 254 | public override byte[] Write() 255 | { 256 | using (BinaryWriter writer = new BinaryWriter(new MemoryStream())) 257 | { 258 | //Write the header. 259 | writer.Write((ushort)0x55AA); 260 | writer.Write((byte)PktCommand.CommandType); 261 | writer.Write((byte)ToAcknowledge); 262 | writer.Write((byte)PktCommand.CommandID); 263 | 264 | //Write the checksum 265 | writer.BaseStream.Flush(); 266 | writer.Write(GetChecksum((writer.BaseStream as MemoryStream).ToArray())); 267 | 268 | writer.BaseStream.Flush(); 269 | return (writer.BaseStream as MemoryStream).ToArray(); 270 | } 271 | } 272 | public override byte[] Encode() 273 | { 274 | throw new NotImplementedException(); 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /WyzeSenseUpgrade/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using System.Buffers.Binary; 5 | using System.Collections.Generic; 6 | 7 | namespace WyzeSenseUpgrade 8 | { 9 | class Program 10 | { 11 | const int SBL_CC2650_MAX_MEMREAD_WORDS = 63; 12 | const int SBL_CC2650_ACCESS_WIDTH_32B = 1; 13 | const uint MAX_PACK_SIZE = 0x3A; 14 | 15 | static FileStream dongleStream; 16 | 17 | static uint DeviceID; 18 | static uint DeviceVersion; 19 | 20 | static async Task Main(string[] args) 21 | { 22 | List argList = new List(args); 23 | if (args.Length > 0) 24 | { 25 | if (File.Exists(args[0])) 26 | { 27 | using (dongleStream = new FileStream(args[0], FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, 4096, true)) 28 | { 29 | Console.WriteLine("Requesting Upgrade Mode "); 30 | Memory requestcc13010Up = new byte[] { 0x07, 0xAA, 0x55, 0x43, 0x03, 0x12, 0x01, 0x57 }; 31 | dongleStream.Write(requestcc13010Up.Span); 32 | 33 | Memory buffer = new byte[0x10]; 34 | int readSize = await dongleStream.ReadAsync(buffer); 35 | 36 | Console.WriteLine($"Read raw data: {DataToString(buffer.Span)}"); 37 | buffer = buffer.Slice(1, buffer.Span[0]); 38 | Console.WriteLine($"Read data: {DataToString(buffer.Span)}"); 39 | 40 | ////Something about verifying connection 41 | //await sendCmd(0, new byte[0]); 42 | //var bootResp = await getCmdResponse(); 43 | //Console.WriteLine($"boot resp = ({bootResp.Item1}, {bootResp.Item2})"); 44 | 45 | //Auto Baud 46 | Console.WriteLine($"Requesting Auto Baud"); 47 | Memory requestBootMode = new byte[] { 0x55, 0x55 }; 48 | dongleStream.Write(requestBootMode.Span); 49 | 50 | var resp = await getCmdResponse(); 51 | Console.WriteLine($"auto baud resp = ({resp.Item1}, {resp.Item2})"); 52 | 53 | 54 | //Testing Ping 55 | Console.WriteLine($"Requesting Ping"); 56 | Memory requestPing = new byte[] { 0x03, 0x20, 0x20 }; 57 | dongleStream.Write(requestPing.Span); 58 | resp = await getCmdResponse(); 59 | Console.WriteLine($"Ping resp = ({resp.Item1}, {resp.Item2})"); 60 | 61 | //Get Device ID 62 | Console.WriteLine($"Requesting Chip ID"); 63 | Memory resqID = new byte[] { 0x03, 0x28, 0x28 }; 64 | dongleStream.Write(resqID.Span); 65 | resp = await getCmdResponse(); 66 | Console.WriteLine($"Chip ID resp = ({resp.Item1}, {resp.Item2})"); 67 | if (resp.Item2) 68 | { 69 | var cmdRespData = await getCmdResponseData(resp.Item3); 70 | if (cmdRespData.Item1) 71 | { 72 | Console.WriteLine($"Raw Chip ID: {DataToString(cmdRespData.Item2.Span)}"); 73 | DeviceID = BinaryPrimitives.ReadUInt32BigEndian(cmdRespData.Item2.Span); 74 | DeviceVersion = (uint)((DeviceID >> 0x1C) < 2 ? 1 : 2); 75 | Console.WriteLine($"Chip ID: {DeviceID:X4} Version: {DeviceVersion}"); 76 | sendCmdAck(true); 77 | } 78 | else 79 | sendCmdAck(false); 80 | } 81 | else 82 | sendCmdAck(false); 83 | 84 | //Get Flash Size 85 | Console.WriteLine("Requesting RAM Size"); 86 | var ramResp = await readMemory32(0x40082250, 1); 87 | 88 | 89 | //Get Flash Size 90 | Console.WriteLine("Requesting Flash Size"); 91 | var flashResp = await readMemory32(0x4003002c, 1); 92 | 93 | Console.WriteLine("Requesting Device ID"); 94 | var ipDeviceID = await readMemory32(0x50001318, 1); 95 | 96 | //DUMP FLASH FIRMWARE 97 | string rwResp; 98 | while (true) 99 | { 100 | Console.WriteLine("Read or Write? "); 101 | rwResp = Console.ReadLine(); 102 | rwResp = rwResp.ToLower(); 103 | if (rwResp == "w" || rwResp == "r") 104 | break; 105 | if (rwResp == "exit") 106 | return; 107 | } 108 | 109 | if (rwResp == "r") 110 | { 111 | 112 | Console.WriteLine("Output name:"); 113 | string outputPath = Console.ReadLine(); 114 | 115 | var flashMem = await readMemory32(0x0, 32768); //32768 * 4 = size of firmware file pulled from my v2 came (and the HMS). 116 | Console.WriteLine("Recv all Flash"); 117 | 118 | using (var writer = new BinaryWriter(File.OpenWrite(outputPath))) 119 | { 120 | writer.Write(flashMem.Item2.ToArray()); 121 | } 122 | Console.WriteLine("File saved"); 123 | 124 | } 125 | else if (rwResp == "w") 126 | { 127 | string firmwarefile; 128 | while (true) 129 | { 130 | Console.WriteLine("Firmare file?:"); 131 | firmwarefile = Console.ReadLine(); 132 | if (File.Exists(firmwarefile)) 133 | break; 134 | Console.WriteLine("Firmware file not found"); 135 | } 136 | 137 | Console.WriteLine("***********************"); 138 | Console.WriteLine("***STARTING TO FLASH***"); 139 | Memory newFirmware = new Memory(File.ReadAllBytes(firmwarefile)); 140 | 141 | (bool, uint) chipCrc32; 142 | uint crc32File = calcCRC32LikeChip(newFirmware.Span); 143 | Console.WriteLine($"File CRC32 (Len: {newFirmware.Length})= {crc32File:X4}"); 144 | 145 | if (await eraseFlash(0x0, (uint)newFirmware.Length)) 146 | { 147 | if (await writeFlashRange(0x0, newFirmware)) 148 | { 149 | 150 | Console.WriteLine("Requesting PostDownload CRC32"); 151 | chipCrc32 = await getCRC32(0x0, 32768 * 4); 152 | if (chipCrc32.Item1) 153 | Console.WriteLine($"PostDownload CRC32 = {chipCrc32.Item2:X4}"); 154 | else 155 | Console.WriteLine("Failed to get PostDownload crc32"); 156 | 157 | Console.WriteLine("*********************"); 158 | Console.WriteLine("***Resetting Device***"); 159 | Console.WriteLine("*********************"); 160 | await cmdReset(); 161 | } 162 | else 163 | { 164 | Console.WriteLine("####Failed to download firmware####"); 165 | } 166 | } 167 | else 168 | { 169 | Console.WriteLine("####Failed to erase flash####"); 170 | } 171 | 172 | Console.WriteLine("Requesting CRC32"); 173 | chipCrc32 = await getCRC32(0x0, 32768 * 4); 174 | if (chipCrc32.Item1) 175 | Console.WriteLine($"CRC32 = {chipCrc32.Item2:X4}"); 176 | else 177 | Console.WriteLine("Failed to get crc32"); 178 | 179 | ////CRC Calc isn't correct. Need to address. 180 | //Console.WriteLine("Testing File CRC32 Calc"); 181 | //Memory fileBytes = new Memory(File.ReadAllBytes(@"mycc1310dump.bin")); 182 | //uint crc32File = calcCRC32LikeChip(fileBytes.Span); 183 | //Console.WriteLine($"File CRC32 (Len: {fileBytes.Length})= {crc32File:X4}"); 184 | } 185 | 186 | } 187 | } 188 | else 189 | Console.WriteLine($"Device doesn't exist at path: {args[0]}"); 190 | } 191 | else 192 | Console.WriteLine($"No device path supplied './WyzeSenseUpgrade [device path]"); 193 | } 194 | private static async Task cmdReset() 195 | { 196 | await sendCmd(0x25, null); 197 | return true; 198 | } 199 | private static async Task writeFlashRange(uint StartAddress, ReadOnlyMemory Data) 200 | { 201 | uint ByteCount = (uint)Data.Length; 202 | uint bytesLeft, dataIdx, bytesInTransfer; 203 | uint transferNumber = 1; 204 | bool bIsRetry = false; 205 | 206 | //tTransfer pvTransfer[2]; 207 | 208 | 209 | uint ui32TotChunks = (ByteCount / MAX_PACK_SIZE); 210 | if (ByteCount % 252 > 0) ui32TotChunks++; 211 | 212 | uint ui32CurrChunk = 0; 213 | 214 | uint ui32BLCfgAddr = 0x0 + 0x20000 - 0x1000 + 0xFDB;// FlashAddr + FlashSize - Page Size + BL Config Offset. 215 | uint ui32BLCfgDataIdx = ui32BLCfgAddr - StartAddress; // This should be 0xC5 otherwise bootloader is disabled. 216 | 217 | if(ui32BLCfgDataIdx <= ByteCount) 218 | { 219 | if(Data.Span[(int)ui32BLCfgDataIdx] != 0xC5) 220 | { 221 | Console.WriteLine("[writeFlashRange] Bootloader being disabled is unsupported at this time."); 222 | } 223 | } 224 | 225 | 226 | //Send Download Command. 227 | if(!await cmdDownload(StartAddress, ByteCount)) 228 | { 229 | Console.WriteLine("[writeFlashRange] Failed to initiate download"); 230 | return false; 231 | } 232 | 233 | var statusResp = await readStatus(); 234 | if(!statusResp.Item1) 235 | { 236 | Console.WriteLine("[writeFlashRange] Failed to read status after download command"); 237 | return false; 238 | } 239 | if(statusResp.Item2 != 0x40) 240 | { 241 | Console.WriteLine($"[writeFlashRange] Error after download command {statusResp.Item2:X2}"); 242 | return false; 243 | } 244 | Console.WriteLine("[writeFlashRange] Starting to write data to flash"); 245 | //Send Data in chunks 246 | bytesLeft = ByteCount; 247 | dataIdx = 0; 248 | while(bytesLeft > 0) 249 | { 250 | bytesInTransfer = Math.Min(MAX_PACK_SIZE, bytesLeft); 251 | 252 | if (!await cmdSendData(Data.Slice((int)dataIdx, (int)bytesInTransfer))) 253 | { 254 | Console.WriteLine($"[writeFlashRange] Error during flash download. Addr: {StartAddress + dataIdx} xfer#: {transferNumber}"); 255 | return false; 256 | } 257 | 258 | statusResp = await readStatus(); 259 | if(!statusResp.Item1) 260 | { 261 | Console.WriteLine($"[writeFlashRange] Error during flash download. Addr: {StartAddress + dataIdx} xfer#: {transferNumber}"); 262 | return false; 263 | } 264 | if(statusResp.Item2 != 0x40) 265 | { 266 | Console.WriteLine($"[writeFlashRange] Device Returns Status {statusResp.Item2} xfer#: {transferNumber}"); 267 | if(bIsRetry) 268 | { 269 | Console.WriteLine($"[writeFlashRange] Error retrying flash download. Addr: {StartAddress + dataIdx} xfer#: {transferNumber}"); 270 | return false; 271 | } 272 | bIsRetry = true; 273 | continue; 274 | } 275 | 276 | bytesLeft -= bytesInTransfer; 277 | dataIdx += bytesInTransfer; 278 | transferNumber++; 279 | bIsRetry = false; 280 | 281 | } 282 | 283 | Console.WriteLine("[writeFlashRange] Successfully Wrote flash"); 284 | return true; 285 | } 286 | private static async Task cmdSendData(ReadOnlyMemory DataChunk) 287 | { 288 | if(DataChunk.Length > MAX_PACK_SIZE) 289 | { 290 | Console.WriteLine($"[cmdSendData] DataChunk Size {DataChunk.Length} exceeds max transfer of {MAX_PACK_SIZE}"); 291 | return false; 292 | } 293 | 294 | await sendCmd(0x24, DataChunk); 295 | 296 | var cmdResp = await getCmdResponse(); 297 | if (!cmdResp.Item1 || !cmdResp.Item2) return false; 298 | 299 | return true; 300 | 301 | } 302 | private static async Task cmdDownload(uint StartAddress, uint ByteCount) 303 | { 304 | Memory packet = new byte[8]; 305 | BinaryPrimitives.WriteUInt32BigEndian(packet.Slice(0).Span, StartAddress); 306 | BinaryPrimitives.WriteUInt32BigEndian(packet.Slice(4).Span, ByteCount); 307 | 308 | 309 | await sendCmd(0x21, packet); 310 | 311 | Console.WriteLine("[cmdDownload] Getting Resp"); 312 | var resp = await getCmdResponse(); 313 | if (!resp.Item1 || !resp.Item2) return false; 314 | 315 | return true; 316 | } 317 | private static async Task eraseFlash(uint StartAddress, uint ByteCount) 318 | { 319 | Memory packet = new byte[4]; 320 | 321 | 322 | uint pageCount = ByteCount / 0x1000; 323 | if (ByteCount % 0x1000 > 0) pageCount++; 324 | 325 | for(int i = 0; i < pageCount; i++) 326 | { 327 | BinaryPrimitives.WriteUInt32BigEndian(packet.Span, (uint)(StartAddress + i * 0x1000)); 328 | 329 | await sendCmd(0x26, packet); 330 | 331 | var cmdResp = await getCmdResponse(); 332 | if (!cmdResp.Item1 || !cmdResp.Item2) return false; 333 | 334 | var statusResp = await readStatus(); 335 | if (statusResp.Item2 != 0x40) 336 | { 337 | Console.WriteLine($"[eraseFlash] Failed to Erase Flash. Page may be locked Addr: {StartAddress + i * 0x1000}"); 338 | return false; 339 | } 340 | } 341 | return true; 342 | } 343 | 344 | private static async Task<(bool, Memory)> readMemory32(uint StartAddress, uint UnitCount) 345 | { 346 | if((StartAddress & 0x03) == 0x03) 347 | { 348 | Console.WriteLine("Address needs to be a multiple of 4"); 349 | return (false, null); 350 | } 351 | 352 | Memory cmdPayload = new byte[6]; 353 | Memory responseData = new byte[UnitCount * 4]; 354 | 355 | uint chunkCount = UnitCount / SBL_CC2650_MAX_MEMREAD_WORDS; 356 | if (UnitCount % SBL_CC2650_MAX_MEMREAD_WORDS > 0) chunkCount++; 357 | 358 | uint remainingCount = UnitCount; 359 | 360 | Console.WriteLine($"[readMemory32] Attempting to read {chunkCount} chunks"); 361 | 362 | for(int i = 0; i < chunkCount; i++) 363 | { 364 | uint dataOffset = (uint)(i * SBL_CC2650_MAX_MEMREAD_WORDS); 365 | uint chunkStart = (uint)(StartAddress + dataOffset); 366 | uint chunkSize = Math.Min(remainingCount, SBL_CC2650_MAX_MEMREAD_WORDS); 367 | remainingCount -= chunkSize; 368 | 369 | BinaryPrimitives.WriteUInt32BigEndian(cmdPayload.Span, chunkStart); 370 | cmdPayload.Span[4] = SBL_CC2650_ACCESS_WIDTH_32B; 371 | cmdPayload.Span[5] = (byte)chunkSize; 372 | 373 | await sendCmd(0x2A, cmdPayload); 374 | 375 | //await readStatus(); 376 | 377 | 378 | Console.WriteLine("[readMemory32] Getting Resp"); 379 | var resp = await getCmdResponse(); 380 | if (!resp.Item1 || !resp.Item2) return (false, null); 381 | 382 | Console.WriteLine("[readMemory32] Getting Resp Data"); 383 | 384 | 385 | var dataResp = await getCmdResponseData(resp.Item3); 386 | if (!dataResp.Item1) 387 | { 388 | sendCmdAck(false); 389 | return (false, null); 390 | } 391 | 392 | Console.WriteLine($"[readMemory32] Raw Resp Data: {DataToString(dataResp.Item2.Span)}"); 393 | 394 | dataResp.Item2.CopyTo(responseData.Slice((int)(dataOffset * 4))); 395 | 396 | sendCmdAck(true); 397 | 398 | } 399 | return (true, responseData); 400 | } 401 | 402 | /// 403 | /// 404 | /// 405 | /// Value1 = cmdResponse success. Value2 = previousCmdAck Value3 = data length 406 | private static async Task<(bool, bool,int)> getCmdResponse() 407 | { 408 | Memory buffer = new byte[3]; 409 | try 410 | { 411 | Console.WriteLine($"[getCmdResponse] Attempting to read 3 bytes"); 412 | await dongleStream.ReadAsync(buffer); 413 | Console.WriteLine($"[getCmdResponse] Read raw data: {DataToString(buffer.Span)}"); 414 | } 415 | catch(Exception e) 416 | { 417 | Console.WriteLine(e.ToString()); 418 | } 419 | if (buffer.Span[0] < 2) 420 | return (false,false, buffer.Span[0]); 421 | else 422 | { 423 | if(buffer.Span[2] == 0xCC) 424 | return (true, true, buffer.Span[0]); 425 | else if(buffer.Span[2] == 0x33) 426 | return (true, false, buffer.Span[0]); 427 | } 428 | return (false, false, buffer.Span[0]); 429 | } 430 | 431 | private static async Task<(bool, Memory)> getCmdResponseData(int DataLength) 432 | { 433 | Memory buffer = new byte[DataLength - 2]; 434 | int readCount = await dongleStream.ReadAsync(buffer); 435 | Console.WriteLine($"[getCmdResponseData] Read raw data: {DataToString(buffer.Span)} ({readCount}/{buffer.Length})"); 436 | 437 | int expectedPackLength = buffer.Span[0] - 2; // sub 2 for packet len and checksum. 438 | byte expectedChecksum = buffer.Span[1]; 439 | int buffIdx = 0; 440 | Memory finalBuff = new byte[expectedPackLength]; 441 | buffer.Slice(2).CopyTo(finalBuff); 442 | buffIdx += readCount - 2; //Sub 2 for packet len and check sum. 443 | 444 | while(expectedPackLength != buffIdx) 445 | { 446 | int remaining = expectedPackLength - buffIdx; 447 | Console.WriteLine($"[getCmdResponseData] Reading additional data: {remaining} remaining"); 448 | int addBufLen = remaining > 0x3E ? 0x3F : (remaining + 1); 449 | 450 | Memory additionalBuff = new byte[addBufLen]; 451 | int readCountAddtl = await dongleStream.ReadAsync(additionalBuff); 452 | 453 | additionalBuff.Slice(1).CopyTo(finalBuff.Slice(buffIdx)); 454 | buffIdx += readCountAddtl - 1; //Sub 1 for data len. 455 | 456 | Console.WriteLine($"Read raw {readCountAddtl} data2: {DataToString(additionalBuff.Span)}"); 457 | } 458 | 459 | 460 | byte ourcheckSum = generateCheckSum(0, finalBuff); 461 | 462 | if(expectedChecksum != ourcheckSum) 463 | { 464 | Console.WriteLine($"[getCmdresponseData] Checksum mismatch {expectedChecksum} != {ourcheckSum} (ours)"); 465 | return (false, null); 466 | } 467 | 468 | return (true, finalBuff); 469 | } 470 | 471 | private static async Task sendCmd(uint Command, ReadOnlyMemory Data) 472 | { 473 | Memory buff = new byte[Data.Length + 3]; 474 | byte checkSum = generateCheckSum(Command, Data); 475 | 476 | buff.Span[0] = (byte)buff.Length; 477 | buff.Span[1] = checkSum; 478 | buff.Span[2] = (byte)Command; 479 | 480 | if (Data.Length > 0) 481 | Data.CopyTo(buff.Slice(3)); 482 | 483 | Console.WriteLine($"[sendCmd] Write Raw: {DataToString(buff.Span)}"); 484 | await dongleStream.WriteAsync(buff); 485 | return true; 486 | } 487 | 488 | private static async Task<(bool, uint)> readStatus() 489 | { 490 | Console.WriteLine($"[readStatus] Sending Read Status"); 491 | await sendCmd(0x23, null); 492 | 493 | Console.WriteLine($"[readStatus] getting Cmd Response"); 494 | var resp = await getCmdResponse(); 495 | Console.WriteLine($"read status resp = ({resp.Item1}, {resp.Item2})"); 496 | if (!resp.Item1 || !resp.Item2) return (false, 0); 497 | 498 | var dataResp = await getCmdResponseData(resp.Item3); 499 | 500 | Console.WriteLine($"[readStatus] raw resp data {DataToString(dataResp.Item2.Span)}"); 501 | 502 | if (!dataResp.Item1) 503 | { 504 | sendCmdAck(false); 505 | return (false, 0); 506 | } 507 | 508 | sendCmdAck(true); 509 | return (true, dataResp.Item2.Span[0]); 510 | } 511 | 512 | private static async Task<(bool, uint)> getCRC32(uint Address, uint Length) 513 | { 514 | Memory packet = new byte[12]; 515 | BinaryPrimitives.WriteUInt32BigEndian(packet.Slice(0).Span, Address); 516 | BinaryPrimitives.WriteUInt32BigEndian(packet.Slice(4).Span, Length); 517 | BinaryPrimitives.WriteUInt32BigEndian(packet.Slice(8).Span, 0); 518 | 519 | 520 | await sendCmd(0x27, packet); 521 | 522 | Console.WriteLine("[getCRC32] Getting Resp"); 523 | var resp = await getCmdResponse(); 524 | if (!resp.Item1 || !resp.Item2) return (false, 0); 525 | 526 | Console.WriteLine("[getCRC32] Getting Resp Data"); 527 | 528 | 529 | var dataResp = await getCmdResponseData(resp.Item3); 530 | 531 | if (!dataResp.Item1) 532 | { 533 | sendCmdAck(false); 534 | return (false, BinaryPrimitives.ReadUInt32BigEndian(dataResp.Item2.Span)); 535 | } 536 | 537 | Console.WriteLine($"[getCRC32] Raw Resp Data: {DataToString(dataResp.Item2.Span)}"); 538 | 539 | sendCmdAck(true); 540 | return(true, BinaryPrimitives.ReadUInt32BigEndian(dataResp.Item2.Span)); 541 | } 542 | 543 | private static uint calcCRC32LikeChip(ReadOnlySpan Data) 544 | { 545 | uint d, ind; 546 | uint acc = 0xFFFFFFFF; 547 | uint[] ulCrcRand32Lut = new uint[16] 548 | { 549 | 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 550 | 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, 551 | 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 552 | 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c 553 | }; 554 | int byteCnt = Data.Length; 555 | //for(int idx = 0; idx < Data.Length; idx++) 556 | int idx = 0; 557 | while(byteCnt-- != 0) 558 | { 559 | d = Data[idx++]; 560 | ind = (acc & 0x0F) ^ (d & 0x0F); 561 | acc = (acc >> 4) ^ ulCrcRand32Lut[ind]; 562 | ind = (acc & 0x0F) ^ (d >> 4); 563 | acc = (acc >> 4) ^ ulCrcRand32Lut[ind]; 564 | } 565 | 566 | return (acc ^ 0xFFFFFFFF); 567 | } 568 | private static byte generateCheckSum(uint Command, ReadOnlyMemory Data) 569 | { 570 | int count = (int)Command; 571 | ReadOnlySpan dataSpan = Data.Span; 572 | 573 | for(int i = 0; i < dataSpan.Length; i++) 574 | { 575 | count += dataSpan[i]; 576 | } 577 | return (byte)count; 578 | } 579 | 580 | private static void sendCmdAck(bool Success) 581 | { 582 | Console.WriteLine($"[sendCmdAck] Sending {(Success ? "Ack" : "Nack")}"); 583 | Span buff = new byte[2]; 584 | //buff[0] = 3; 585 | buff[0] = 0; 586 | buff[1] =(byte)(Success ? 0xCC : 0x33); 587 | 588 | dongleStream.Write(buff); 589 | 590 | } 591 | private static string DataToString(ReadOnlySpan data) 592 | { 593 | string byteString = ""; 594 | for (int i = 0; i < data.Length; i++) 595 | { 596 | byteString += string.Format("{0:X2} ", data[i]); 597 | } 598 | return byteString.TrimEnd(' '); 599 | } 600 | } 601 | } 602 | -------------------------------------------------------------------------------- /WyzeSenseCore/WyzeDongle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Buffers.Binary; 7 | using System.Collections.Generic; 8 | using Microsoft.Extensions.Logging; 9 | using System.Linq; 10 | 11 | namespace WyzeSenseCore 12 | { 13 | public sealed class WyzeDongle : IWyzeDongle, IDisposable 14 | { 15 | private const int CMD_TIMEOUT = 5000; 16 | 17 | private ByteBuffer dataBuffer; 18 | 19 | private ManualResetEvent waitHandle = new ManualResetEvent(false); 20 | private CancellationTokenSource dongleTokenSource; 21 | private CancellationTokenSource dongleScanTokenSource; 22 | 23 | private DataProcessor dataProcessor; 24 | 25 | private string donglePath; 26 | 27 | private FileStream dongleRead; 28 | private FileStream dongleWrite; 29 | 30 | private WyzeDongleState dongleState; 31 | private bool commandedLEDState; 32 | 33 | private Dictionary sensors; 34 | private Dictionary tempScanSensors; 35 | private int expectedSensorCount; 36 | private int actualSensorCount; 37 | 38 | private string lastAddedSensorMAC = ""; 39 | private WyzeSensor lastAddedSensor; 40 | 41 | public event EventHandler OnAddSensor; 42 | public event EventHandler OnRemoveSensor; 43 | public event EventHandler OnSensorEvent; 44 | public event EventHandler OnDongleStateChange; 45 | 46 | private IWyzeSenseLogger _logger; 47 | 48 | 49 | public WyzeDongle( IWyzeSenseLogger logger) 50 | { 51 | _logger = logger; 52 | sensors = new(); 53 | dongleState = new(); 54 | dataBuffer = new(0x80, MaxCapacity: 1024); 55 | } 56 | public void Dispose() 57 | { 58 | dongleWrite?.Dispose(); 59 | dongleRead?.Dispose(); 60 | } 61 | public bool OpenDevice(string devicePath) 62 | { 63 | donglePath = devicePath; 64 | try 65 | { 66 | dongleRead = new FileStream(donglePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, 4096, true); 67 | dongleWrite = new FileStream(donglePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, 4096, true); 68 | } 69 | catch (Exception e) 70 | { 71 | _logger.LogError($"[Dongle][OpenDevice] {e.ToString()}"); 72 | return false; 73 | } 74 | return true; 75 | } 76 | public void Stop() 77 | { 78 | dongleTokenSource.Cancel(); 79 | dongleWrite.Close(); 80 | dongleRead.Close(); 81 | } 82 | public async Task StartAsync(CancellationToken cancellationToken) 83 | { 84 | _logger.LogInformation("Starting Dongle"); 85 | 86 | if (dongleRead == null || dongleWrite == null) throw new Exception("Device Not open"); 87 | 88 | 89 | 90 | dongleTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 91 | 92 | dataProcessor = new(dongleTokenSource.Token, dongleTokenSource.Token); 93 | dongleScanTokenSource = new(); 94 | _logger.LogInformation($"[Dongle][StartAsync] Opening USB Device {donglePath}"); 95 | 96 | _logger.LogDebug("[Dongle][StartAsync] USB Device opened"); 97 | 98 | Task process = UsbProcessingAsync(); 99 | 100 | _logger.LogInformation("[Dongle][StartAsync] Requesting Device Type"); 101 | await this.WriteCommandAsync(BasePacket.RequestDeviceType()); 102 | 103 | //TODO: Research this feature to better understand what its doing. 104 | _logger.LogInformation("[Dongle][StartAsync] Requesting Enr"); 105 | byte[] pack = new byte[16]; 106 | for (int i = 0; i < pack.Length; i++) 107 | pack[i] = 30; 108 | await this.WriteCommandAsync(BasePacket.RequestEnr(pack)); 109 | 110 | _logger.LogInformation("[Dongle][StartAsync] Requesting MAC"); 111 | await this.WriteCommandAsync(BasePacket.RequestMAC()); 112 | 113 | _logger.LogInformation("[Dongle][StartAsync] Requesting Version"); 114 | await this.WriteCommandAsync(BasePacket.RequestVersion()); 115 | 116 | _logger.LogInformation("[Dongle][StartAsync] Finishing Auth"); 117 | await this.WriteCommandAsync(BasePacket.FinishAuth()); 118 | 119 | //Request Sensor List 120 | await RequestRefreshSensorListAsync(); 121 | 122 | } 123 | public async void SetLedAsync(bool On) 124 | { 125 | _logger.LogDebug($"[Dongle][SetLedAsync] Setting LED to: {On}"); 126 | commandedLEDState = On; 127 | if (On) 128 | await this.WriteCommandAsync(BasePacket.SetLightOn()); 129 | else 130 | await this.WriteCommandAsync(BasePacket.SetLightOff()); 131 | } 132 | public async Task StartScanAsync(int Timeout = 60 * 1000) 133 | { 134 | _logger.LogDebug($"[Dongle][StartScanAsync] Starting Inclusion for {Timeout / 1000} seconds"); 135 | await this.WriteCommandAsync(BasePacket.RequestEnableScan()); 136 | try 137 | { 138 | await Task.Delay(Timeout, dongleScanTokenSource.Token); 139 | await StopScanAsync(); 140 | } 141 | catch (TaskCanceledException tce) 142 | { 143 | dongleScanTokenSource = new(); 144 | } 145 | } 146 | public async Task StopScanAsync() 147 | { 148 | _logger.LogDebug($"[Dongle][StopScanAsync] Stopping Inclusion Scan"); 149 | await this.WriteCommandAsync(BasePacket.RequestDisableScan()); 150 | dongleScanTokenSource.Cancel(); 151 | } 152 | public async Task RequestRefreshSensorListAsync() 153 | { 154 | _logger.LogDebug($"[Dongle][RequestRefreshSensorListAsync] Sending Sensor Count Request"); 155 | await this.WriteCommandAsync(BasePacket.RequestSensorCount()); 156 | } 157 | public async Task DeleteSensorAsync(string MAC) 158 | { 159 | _logger.LogDebug($"[Dongle][DeleteSensor] Issuing sensor delete: {MAC}"); 160 | await WriteCommandAsync(BasePacket.DeleteSensor(MAC)); 161 | } 162 | public async Task RequestCC1310Update() 163 | { 164 | _logger.LogDebug($"[Dongle][RequestCC1310Update] Asking to upgrade cc1310."); 165 | await WriteCommandAsync(BasePacket.UpdateCC1310()); 166 | } 167 | private async Task UsbProcessingAsync() 168 | { 169 | _logger.LogInformation("[Dongle][UsbProcessingAsync] Beginning to process USB data"); 170 | Memory dataReadBuffer = new byte[0x40]; 171 | Memory headerBuffer = new byte[5]; 172 | try 173 | { 174 | while (!dongleTokenSource.Token.IsCancellationRequested) 175 | { 176 | while (dataBuffer.Peek(headerBuffer.Span)) 177 | { 178 | _logger.LogTrace($"[Dongle][UsbProcessingAsync] dataBuffer contains enough bytes for header {dataBuffer.Size}"); 179 | ushort magic = BitConverter.ToUInt16(headerBuffer.Slice(0, 2).Span); 180 | if (magic != 0xAA55) 181 | { 182 | _logger.LogError($"[Dongle][UsbProcessingAsync] Unexpected header bytes {magic:X4}"); 183 | dataBuffer.Burn(1); 184 | continue; 185 | } 186 | if (headerBuffer.Span[4] == 0xff)//If this is the ack packet 187 | { 188 | dataBuffer.Burn(7); 189 | //Acknowledge the command that we sent. Do we need to track which packet was last sent and verify? 190 | this.waitHandle.Set(); 191 | _logger.LogTrace($"[Dongle][UsbProcessingAsync] Received dongle ack packet for {headerBuffer.Span[3]} burning 7 bytes, {dataBuffer.Size} remain"); 192 | continue; 193 | } 194 | 195 | byte dataLen = headerBuffer.Span[3]; 196 | 197 | if (dataBuffer.Size >= (dataLen + headerBuffer.Length - 1)) 198 | { 199 | //A complete packet has been received 200 | Memory dataPack = new Memory(new byte[dataLen + headerBuffer.Length - 1]); 201 | dataBuffer.Dequeue(dataPack.Span); 202 | 203 | 204 | if (dataPack.Span[2] == (byte)Command.CommandTypes.TYPE_ASYNC) 205 | { 206 | await this.WriteAsync(BasePacket.AckPacket(dataPack.Span[4])); 207 | _logger.LogTrace($"[Dongle][UsbProcessingAsync] Acknowledging packet type 0x{dataPack.Span[4]:X2}"); 208 | } 209 | dataReceived(dataPack.Span); 210 | } 211 | else 212 | { 213 | _logger.LogDebug($"[Dongle][UsbProcessingAsync] Incomplete packet received {dataBuffer.Size}/{dataLen + headerBuffer.Length - 1}"); 214 | break; 215 | } 216 | } 217 | 218 | _logger.LogTrace("[Dongle][UsbProcessingAsync] Preparing to receive bytes"); 219 | int result = -1; 220 | try 221 | { 222 | result = await dongleRead.ReadAsync(dataReadBuffer, dongleTokenSource.Token); 223 | } 224 | catch (TaskCanceledException tce) 225 | { 226 | _logger.LogInformation("[Dongle][UsbProcessingAsync] ReadAsync was cancelled"); 227 | continue; 228 | } 229 | 230 | if (result == 0) 231 | { 232 | _logger.LogError($"[Dongle][UsbProcessingAsync] End of file stream reached"); 233 | break; 234 | } 235 | else if (result >= 0) 236 | { 237 | _logger.LogTrace($"[Dongle][UsbProcessingAsync] {dataReadBuffer.Span[0]} Bytes Read Raw: {DataToString(dataReadBuffer.Slice(1, dataReadBuffer.Span[0]).Span)}"); 238 | dataBuffer.Queue(dataReadBuffer.Slice(1, dataReadBuffer.Span[0]).Span); 239 | _logger.LogTrace("[Dongle][UsbProcessingAsync] Finished processing received bytes"); 240 | 241 | } 242 | } 243 | } 244 | catch (Exception e) 245 | { 246 | _logger.LogError($"[Dongle][UsbProcessingAsync] {e.ToString()}"); 247 | } 248 | 249 | 250 | _logger.LogInformation("[Dongle][UsbProcessingAsync] Exiting Processing loop"); 251 | } 252 | 253 | private async Task WriteCommandAsync(BasePacket Pack) 254 | { 255 | waitHandle.Reset(); 256 | await this.WriteAsync(Pack); 257 | if (!waitHandle.WaitOne(CMD_TIMEOUT)) 258 | _logger.LogWarning("[Dongle][WriteCommandAsync] Command Timed out - did not receive response in alloted time"); 259 | else 260 | _logger.LogInformation("[Dongle][WriteCommandAsync] Successfully Wrote command"); 261 | } 262 | private async Task WriteAsync(BasePacket Pack) 263 | { 264 | byte[] data = Pack.Write(); 265 | byte[] preppedData = new byte[data.Length + 1]; 266 | preppedData[0] = (byte)data.Length; 267 | Array.Copy(data, 0, preppedData, 1, data.Length); 268 | try 269 | { 270 | await dongleWrite.WriteAsync(preppedData, 0, preppedData.Length); 271 | await dongleWrite.FlushAsync(); 272 | _logger.LogTrace($"[Dongle][WriteAsync] Successfully wrote {preppedData.Length} bytes: {DataToString(preppedData)}"); 273 | } 274 | catch (Exception e) 275 | { 276 | _logger.LogError($"[Dongle][WriteAsync] {e.ToString()}"); 277 | } 278 | 279 | } 280 | 281 | private void refreshSensors() 282 | { 283 | List toRem = new List(); 284 | //Look for existing sensors that aren't actually bound 285 | foreach (var sensor in sensors.Values) 286 | { 287 | if (!tempScanSensors.ContainsKey(sensor.MAC)) 288 | toRem.Add(sensor); 289 | } 290 | foreach (var sensor in toRem) 291 | { 292 | sensors.Remove(sensor.MAC); 293 | if (OnRemoveSensor != null) 294 | this.dataProcessor.Queue(() => OnRemoveSensor.Invoke(this,sensor)); 295 | } 296 | 297 | //Add sensors that don't already exist 298 | foreach (var sensor in tempScanSensors.Values) 299 | { 300 | if (!sensors.ContainsKey(sensor.MAC)) 301 | { 302 | sensors.Add(sensor.MAC, sensor); 303 | if (OnAddSensor != null) 304 | this.dataProcessor.Queue(() => OnAddSensor.Invoke(this,sensor)); 305 | } 306 | } 307 | } 308 | 309 | private string DataToString(ReadOnlySpan data) 310 | { 311 | string byteString = ""; 312 | for (int i = 0; i < data.Length; i++) 313 | { 314 | byteString += string.Format("{0:X2} ", data[i]); 315 | } 316 | return byteString.TrimEnd(' '); 317 | } 318 | 319 | private void dataReceived(ReadOnlySpan Data) 320 | { 321 | _logger.LogInformation($"[Dongle][dataReceived] {Data.Length} Bytes received: {DataToString(Data)}"); 322 | 323 | switch ((Command.CommandIDs)Data[4]) 324 | { 325 | case Command.CommandIDs.NotifySensorStart: 326 | string mac = ASCIIEncoding.ASCII.GetString(Data.Slice(6, 8)); 327 | byte type = Data[14]; 328 | byte version = Data[15]; 329 | _logger.LogDebug($"[Dongle][dataReceived] New Sensor: {mac} Type: {(WyzeSensorType)type} Version: {version}"); 330 | lastAddedSensor = new WyzeSensor() 331 | { 332 | MAC = mac, 333 | Type = (WyzeSensorType)type, 334 | Version = version 335 | }; 336 | lastAddedSensorMAC = mac; 337 | WriteCommandAsync(BasePacket.SetSensorRandomDate(mac)).FireAndForget(_logger); 338 | break; 339 | case Command.CommandIDs.SensorRandomDateResp: 340 | string rmac = ASCIIEncoding.ASCII.GetString(Data.Slice(6, 8)); 341 | string rand = ASCIIEncoding.ASCII.GetString(Data.Slice(14, 16)); 342 | byte isSuccess = Data[30]; 343 | //byte 31 is version? 344 | 345 | //TODO: Do I need to store this key, what is it's purpose. The data doesn't appear to be encrypted - maybe a future update? 346 | if (isSuccess < 0 || isSuccess > 0xC) 347 | _logger.LogError($"[Dongle][dataReceived] Random Date Seed Failed for : {rmac}"); 348 | else 349 | _logger.LogDebug($"[Dongle][dataReceived] Random Date Resp: {rmac} Random: {rand}"); 350 | 351 | _logger.LogInformation($"[Dongle][dataReceived] Verifying Sensor: {lastAddedSensorMAC}"); 352 | 353 | WriteCommandAsync(BasePacket.VerifySensor(lastAddedSensorMAC)).FireAndForget(_logger); 354 | break; 355 | case Command.CommandIDs.NotifyEventLog: 356 | //timestamp is Slice(5,8); 357 | eventReceived(Data.Slice(14)); 358 | break; 359 | case Command.CommandIDs.NotifySensorAlarm: 360 | switch ((WyzeEventType)Data[0x0D]) 361 | { 362 | case WyzeEventType.Alarm: 363 | var alarmEvent = new WyzeSenseEvent() 364 | { 365 | Sensor = getSensor(ASCIIEncoding.ASCII.GetString(Data.Slice(0x0E, 8)), (WyzeSensorType)Data[0x16]), 366 | ServerTime = DateTime.Now, 367 | EventType = WyzeEventType.Alarm, 368 | Data = new Dictionary() 369 | { 370 | { "Battery" , Data[0x18] }, 371 | { "Signal" , Data[0x1E] } 372 | } 373 | }; 374 | _logger.LogTrace($"[Dongle][dataReceived] Sensor Type: {Data[0x16]}"); 375 | if (alarmEvent.Sensor.Type == WyzeSensorType.Motion || alarmEvent.Sensor.Type == WyzeSensorType.Motionv2) 376 | alarmEvent.Data.Add("Motion", Data[0x1B]); 377 | else if (alarmEvent.Sensor.Type == WyzeSensorType.Switch || alarmEvent.Sensor.Type == WyzeSensorType.SwitchV2) 378 | alarmEvent.Data.Add("Contact", Data[0x1B]); 379 | else 380 | alarmEvent.Data.Add("State", Data[0x1B]); 381 | 382 | _logger.LogInformation($"[Dongle][dataReceived] NotifySensorAlarm - Alarm: {alarmEvent.ToString()}"); 383 | if (OnSensorEvent != null) 384 | dataProcessor.Queue(() => OnSensorEvent.Invoke(this, alarmEvent)); 385 | 386 | 387 | break; 388 | case WyzeEventType.Climate: 389 | var tempEvent = new WyzeSenseEvent() 390 | { 391 | Sensor = getSensor(ASCIIEncoding.ASCII.GetString(Data.Slice(0x0E, 8)), WyzeSensorType.Climate), 392 | ServerTime = DateTime.Now, 393 | EventType = WyzeEventType.Status, 394 | Data = new Dictionary() 395 | { 396 | { "Temperature", Data[0x1B] }, 397 | { "Humidity", Data[0x1D] }, 398 | { "Battery" , Data[0x18] }, 399 | { "Signal" , Data[0x20] }, 400 | } 401 | }; 402 | _logger.LogInformation($"[Dongle][dataReceived] NotifySensorAlarm - Temp: {tempEvent.ToString()}"); 403 | if (OnSensorEvent != null) 404 | { 405 | dataProcessor.Queue(() => OnSensorEvent.Invoke(this, tempEvent)); 406 | } 407 | break; 408 | default: 409 | _logger.LogDebug($"[Dongle][dataReceived] Unhandled SensorAlarm {Data[0x0D]:X2}\r\n\t{DataToString(Data)}"); 410 | break; 411 | 412 | } 413 | break; 414 | case Command.CommandIDs.RequestSyncTime: 415 | _logger.LogDebug("[Dongle][dataReceived] Dongle is requesting time"); 416 | break; 417 | case Command.CommandIDs.VerifySensorResp: 418 | _logger.LogDebug("[Dongle][dataReceived] Verify Sensor Resp Ack"); 419 | if(lastAddedSensor == null) 420 | { 421 | _logger.LogDebug("[Dongle][dataReceived] Last Added Sensor is null"); 422 | break; 423 | } 424 | dataProcessor.Queue(() => OnAddSensor.Invoke(this, lastAddedSensor)); 425 | break; 426 | case Command.CommandIDs.StartStopScanResp: 427 | case Command.CommandIDs.AuthResp: 428 | case Command.CommandIDs.SetLEDResp: 429 | case Command.CommandIDs.DeleteSensorResp: 430 | case Command.CommandIDs.GetDeviceTypeResp: 431 | case Command.CommandIDs.RequestDongleVersionResp: 432 | case Command.CommandIDs.RequestEnrResp: 433 | case Command.CommandIDs.RequestMACResp: 434 | case Command.CommandIDs.GetSensorCountResp: 435 | case Command.CommandIDs.GetSensorListResp: 436 | case Command.CommandIDs.UpdateCC1310Resp: 437 | this.commandCallbackReceived(Data); 438 | break; 439 | case Command.CommandIDs.KeyPadEvent: 440 | keypadDataReceived(Data.Slice(5)); 441 | break; 442 | default: 443 | _logger.LogDebug($"[Dongle][dataReceived] Data handler not assigned {(Command.CommandIDs)Data[4]} (Hex={string.Format("{0:X2}", Data[4])})\r\n\t{DataToString(Data)}"); 444 | break; 445 | 446 | } 447 | } 448 | private void eventReceived(ReadOnlySpan Data) 449 | { 450 | switch (Data[0]) 451 | { 452 | case 0x14: 453 | _logger.LogDebug($"[Dongle][eventReceived] Dongle Auth State: {Data[1]}"); 454 | this.dongleState.AuthState = Data[1]; 455 | if (this.OnDongleStateChange != null) 456 | this.dataProcessor.Queue(() => this.OnDongleStateChange.Invoke(this, this.dongleState)); 457 | break; 458 | case 0x1C: 459 | _logger.LogDebug($"[Dongle][eventReceived] Dongle Scan State Set: {Data[1]}"); 460 | this.dongleState.IsInclusive = Data[1] == 1 ? true : false; 461 | if (this.OnDongleStateChange != null) 462 | this.dataProcessor.Queue(() => this.OnDongleStateChange.Invoke(this, this.dongleState)); 463 | break; 464 | case 0x21: 465 | //This packet is deformed, cuts the last byte of the MAC off. 466 | string partialMac = ASCIIEncoding.ASCII.GetString(Data.Slice(1, 7)); 467 | _logger.LogDebug($"[Dongle][eventReceived] Set Random Date Confirmation Partial Mac:{partialMac}"); 468 | if (!lastAddedSensorMAC.StartsWith(partialMac)) 469 | { 470 | _logger.LogError($"[Dongle][eventReceived] Random Date Confirmation is not for the correct sensor. Expected: {lastAddedSensorMAC}, Received Partial : {partialMac}"); 471 | return; 472 | } 473 | break; 474 | case 0x23: 475 | _logger.LogDebug($"[Dongle][eventReceived] Verify Sensor Event {ASCIIEncoding.ASCII.GetString(Data.Slice(1, 8))}"); 476 | break; 477 | case 0x25: 478 | _logger.LogDebug($"[Dongle][eventReceived] Dongle Deleted {ASCIIEncoding.ASCII.GetString(Data.Slice(1,8))}"); 479 | break; 480 | case 0xA2: 481 | string mac = ASCIIEncoding.ASCII.GetString(Data.Slice(1, 8)); 482 | ushort eventID = BinaryPrimitives.ReadUInt16BigEndian(Data.Slice(11, 2)); 483 | _logger.LogDebug($"[Dongle][eventReceived] Event - {mac} SensorType:{(WyzeSensorType)Data[9]} State:{Data[10]} EventNumber:{eventID}"); 484 | break; 485 | case 0xA3: 486 | string newmac = ASCIIEncoding.ASCII.GetString(Data.Slice(1, 8)); 487 | byte type = Data[9]; 488 | byte version = Data[10]; 489 | _logger.LogDebug($"[Dongle][dataReceived] New Sensor: {newmac} Type: {(WyzeSensorType)type} Version: {version}"); 490 | break; 491 | default: 492 | _logger.LogError($"[Dongle][eventReceived] Unknown event ID: {string.Format("{0:X2}", Data[0])}\r\n\t{DataToString(Data)}"); 493 | break; 494 | } 495 | } 496 | private void commandCallbackReceived(ReadOnlySpan Data) 497 | { 498 | _logger.LogInformation("[Dongle][commandCallback] Receiving command response"); 499 | 500 | Command.CommandIDs cmdID = (Command.CommandIDs)Data[4]; 501 | switch (cmdID) 502 | { 503 | case Command.CommandIDs.UpdateCC1310Resp: 504 | _logger.LogError($"[Dongle][commandCallback] READY TO START CC1310 UPGRADE"); 505 | break; 506 | case Command.CommandIDs.GetSensorCountResp: 507 | expectedSensorCount = Data[5]; 508 | _logger.LogDebug($"[Dongle][commandCallback] There are {expectedSensorCount} sensor bound to this dongle"); 509 | this.tempScanSensors = new Dictionary(); 510 | actualSensorCount = 0; 511 | this.WriteCommandAsync(BasePacket.RequestSensorList((byte)expectedSensorCount)).FireAndForget(_logger); 512 | break; 513 | case Command.CommandIDs.GetSensorListResp: 514 | _logger.LogDebug($"[Dongle][commandCallback] GetSensorResp"); 515 | _logger.LogTrace($"[Dongle][commandCallback] {DataToString(Data)}"); 516 | WyzeSensor sensor = new WyzeSensor(Data); 517 | tempScanSensors.TryAdd(sensor.MAC, sensor); 518 | actualSensorCount++; 519 | if (actualSensorCount == expectedSensorCount) 520 | refreshSensors(); 521 | break; 522 | case Command.CommandIDs.DeleteSensorResp: 523 | string mac = ASCIIEncoding.ASCII.GetString(Data.Slice(5, 8)); 524 | byte delStatue = Data[13]; 525 | _logger.LogDebug($"[Dongle][commandCallback] Delete Sensor Resp: {mac}"); 526 | if (sensors.TryGetValue(mac, out var delsensor)) 527 | { 528 | if (OnRemoveSensor != null) 529 | dataProcessor.Queue(() => OnRemoveSensor.Invoke(this, delsensor)); 530 | sensors.Remove(mac); 531 | } 532 | break; 533 | case Command.CommandIDs.SetLEDResp: 534 | if (Data[5] == 0xff) this.dongleState.LEDState = commandedLEDState; 535 | _logger.LogDebug($"[Dongle][commandCallback] Dongle LED Feedback: {dongleState.LEDState}"); 536 | if (OnDongleStateChange != null) 537 | this.dataProcessor.Queue(() => OnDongleStateChange.Invoke(this,dongleState)); 538 | break; 539 | case Command.CommandIDs.GetDeviceTypeResp: 540 | dongleState.DeviceType = Data[5]; 541 | _logger.LogDebug($"[Dongle][commandCallback] Dongle Device Type: {dongleState.DeviceType}"); 542 | waitHandle.Set(); 543 | break; 544 | case Command.CommandIDs.RequestDongleVersionResp: 545 | dongleState.Version = ASCIIEncoding.ASCII.GetString(Data.Slice(5, (Data[3] - 3))); 546 | _logger.LogDebug($"[Dongle][commandCallback] Dongle Version: {dongleState.Version}"); 547 | break; 548 | case Command.CommandIDs.RequestEnrResp: 549 | dongleState.ENR = Data.Slice(5, (Data[3] - 3)).ToArray(); 550 | _logger.LogDebug($"[Dongle][commandCallback] Dongle ENR: {DataToString(dongleState.ENR)}"); 551 | waitHandle.Set(); 552 | break; 553 | case Command.CommandIDs.RequestMACResp: 554 | dongleState.MAC = ASCIIEncoding.ASCII.GetString(Data.Slice(5, (Data[3] - 3))); 555 | _logger.LogDebug($"[Dongle][commandCallback] Dongle MAC: {dongleState.MAC}"); 556 | waitHandle.Set(); 557 | break; 558 | case Command.CommandIDs.AuthResp: 559 | _logger.LogDebug("[Dongle][commandCallback] Dongle Auth Resp"); 560 | break; 561 | case Command.CommandIDs.StartStopScanResp: 562 | _logger.LogDebug("[Dongle][commandCallback] Start/Stop Scan Resp"); 563 | break; 564 | default: 565 | _logger.LogDebug($"[Dongle][commandCallback] Unknown Command ID {cmdID}\r\n\t{ DataToString(Data)}"); 566 | break; 567 | } 568 | 569 | } 570 | private void keypadDataReceived(ReadOnlySpan Data) 571 | { 572 | byte eventType = Data[0]; //Used to send P1301 or P1302 to wyze server. No idea. 573 | string MAC = ASCIIEncoding.ASCII.GetString(Data.Slice(1, 8)); 574 | var var1 = Data[0xA]; 575 | var var2 = Data[0xe]; 576 | var var3 = Data[var1 + 0xB]; //P1303 - battery? 577 | var var4 = Data[0xB]; 578 | var var5 = Data[0xC];//P1304 - signal? 579 | var var6 = Data[0xD]; 580 | 581 | switch (Data[0xE]) 582 | { 583 | case 2: 584 | case 0xA: 585 | //WyzeKeyPadEvent kpEvent = new(Data); 586 | var keypadEvent = new WyzeSenseEvent() 587 | { 588 | Sensor = getSensor(MAC, WyzeSensorType.KeyPad), 589 | ServerTime = DateTime.Now, 590 | EventType = (Data[0x0E] == 0x02 ? WyzeEventType.UserAction : WyzeEventType.Alarm), 591 | Data = new Dictionary() 592 | { 593 | 594 | { "Battery", Data[0x0C] }, 595 | { "Signal", Data[Data[0x0A] + 0x0B] } 596 | } 597 | }; 598 | if (Data[0x0E] == 0x02) 599 | { 600 | keypadEvent.Data.Add("Mode", Data[0x0F]+1); 601 | keypadEvent.Data.Add("ModeName", ((WyzeKeyPadState)Data[0x0F]+1).ToString()); 602 | } 603 | else 604 | keypadEvent.Data.Add("Motion", Data[0x0F]); 605 | 606 | _logger.LogInformation($"[Dongle][keypadDataReceived] KeypadEvent - {keypadEvent}"); 607 | if (OnSensorEvent != null) 608 | dataProcessor.Queue(() => OnSensorEvent.Invoke(this, keypadEvent)); 609 | break; 610 | case 6: 611 | //Received when you start entering keypad pin code. 612 | _logger.LogWarning($"[Dongle][KeypadDataReceived] Keypad({MAC}) Request Profile status - is it requesting a response??"); 613 | break; 614 | case 8: 615 | var pinEvent = new WyzeSenseEvent() 616 | { 617 | Sensor = getSensor(MAC, (WyzeSensorType)Data[0x0E]), 618 | ServerTime = DateTime.Now, 619 | EventType = WyzeEventType.UserAction, 620 | Data = new() 621 | { 622 | { "Battery", Data[0x0C] },//Not confident 623 | { "Signal", Data[Data[0xA] + 0xB] } 624 | } 625 | }; 626 | 627 | //Extract pin. 628 | ReadOnlySpan pinRaw = Data.Slice(0xF, Data[0xA] - 6); 629 | string Pin = ""; 630 | foreach (byte b in pinRaw) 631 | Pin += b.ToString(); 632 | pinEvent.Data.Add("Pin", Pin); 633 | 634 | _logger.LogInformation($"[Dongle][KeypadDataReceived] Pin event received"); 635 | if(OnSensorEvent != null) 636 | dataProcessor.Queue(() => OnSensorEvent.Invoke(this, pinEvent)); 637 | break; 638 | case 0xC: 639 | _logger.LogInformation($"[Dongle][KeypadDataReceived] Keypad({MAC}) Some sort of alarm event?"); 640 | break; 641 | case 0x12: 642 | var dongleWaterEvent = new WyzeSenseEvent() 643 | { 644 | Sensor = getSensor(MAC, WyzeSensorType.Water), 645 | ServerTime = DateTime.Now, 646 | EventType = WyzeEventType.Alarm, //Alarm. Not sure if water sensors check in periodically. 647 | Data = new Dictionary() 648 | { 649 | { "HasExtension", Data[0x11] }, 650 | { "ExtensionWater", Data[0x10] }, 651 | { "Water", Data[0x0F] }, 652 | { "Battery" , Data[0x0C] },//Not confident 653 | { "Signal" , Data[0x14] } 654 | } 655 | }; 656 | _logger.LogInformation($"[Dongle][keypadDataReceived] WaterEvent -: {dongleWaterEvent}"); 657 | if (OnSensorEvent != null) 658 | dataProcessor.Queue(() => OnSensorEvent.Invoke(this, dongleWaterEvent)); 659 | 660 | break; 661 | default: break; 662 | } 663 | } 664 | public Task GetSensorAsync() 665 | { 666 | return Task.FromResult(sensors.Select(index => index.Value).ToArray()); 667 | } 668 | 669 | public WyzeDongleState GetDongleState() 670 | { 671 | return this.dongleState; 672 | } 673 | private WyzeSensor getSensor(string MAC, WyzeSensorType Type) 674 | { 675 | if (sensors.TryGetValue(MAC, out var sensor)) 676 | { 677 | sensor.Type = Type; 678 | return sensor; 679 | } 680 | return new WyzeSensor() 681 | { 682 | MAC = MAC, 683 | Type = Type, 684 | Version = 0 685 | }; 686 | } 687 | 688 | } 689 | } 690 | --------------------------------------------------------------------------------