using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using Serilog; using System.Diagnostics; using YaleAccess.Models; using YaleAccess.Models.Options; using YaleAccess.Services.Interfaces; using ZWaveJS.NET; using ZWaveOptions = YaleAccess.Models.Options.ZWaveOptions; namespace YaleAccess.Services { public class YaleAccessor : IYaleAccessor, IDisposable { #region Dispose Logic private bool disposedValue; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { driver?.Destroy(); } disposedValue = true; driver = null; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } #endregion Dispose Logic private Driver? driver = null; private readonly ZWaveNode lockNode = null!; public YaleAccessor(IOptions zwave, IOptions device) { // Retrive options from configuration ZWaveOptions zwaveOptions = zwave.Value; DevicesOptions devicesOptions = device.Value; // Create a new driver instance driver = new Driver(new Uri(zwaveOptions.Url), zwaveOptions.SchemaVersion); // Start the driver driver.Start(); // Flag to indicate if the driver is ready to use bool isReady = false; // Message and flag to indicate if there was an error starting the driver string? message = null; // Subscribe to the driver ready event driver.DriverReady += () => { Log.Logger.Information("Z-Wave driver started successfully."); isReady = true; }; driver.StartupErrorEvent += (error) => { Log.Logger.Error("Error starting the driver: {error}", error); message = error; }; // Wait for the driver to be ready while (!isReady && message == null) { // Sleep for a short time to avoid busy waiting Thread.Sleep(100); } // If there was an error starting the driver, throw an exception if (message != null) { Log.Logger.Error("Failed to start the driver. Error message: {message}", message); throw new Exception($"Failed to start the driver. Error message: {message}"); } // Get the lock node from the driver lockNode = driver.Controller.Nodes.Get(devicesOptions.YaleLockNodeId); } public async Task GetCodeInformationAsync(int userCodeId) { Stopwatch stopwatch = new(); stopwatch.Start(); // Log the start of the operation Log.Logger.Debug("Retrieving user code information for user code ID: {userCodeId}", userCodeId); // Setup the two tasks to get the values we need CMDResult status = await lockNode.GetValue(GetUserStatusValue(userCodeId)); Log.Logger.Debug("Retrieved user code status for user code ID: {userCodeId} in {ElapsedMilliseconds} ms", userCodeId, stopwatch.ElapsedMilliseconds); CMDResult code = await lockNode.GetValue(GetUserCodeValue(userCodeId)); Log.Logger.Debug("Retrieved user code value for user code ID: {userCodeId} in {ElapsedMilliseconds} ms", userCodeId, stopwatch.ElapsedMilliseconds); // Covert the result to a YaleUserCode object YaleUserCode yaleUserCode = new() { Id = userCodeId, Code = GetUserCodeValue(code), Status = GetUserStatusValue(status), IsHome = false }; stopwatch.Stop(); Log.Logger.Debug("Retrieved user code information for user code ID: {userCodeId} in {ElapsedMilliseconds} ms", userCodeId, stopwatch.ElapsedMilliseconds); // Return the user code information return yaleUserCode; } public async Task SetUserCode(int userCodeId, string code) { Stopwatch stopwatch = new(); stopwatch.Start(); // Log the start of the operation Log.Logger.Debug("Setting user code for user code ID: {userCodeId} to {code}", userCodeId, code); // Setup the set value task CMDResult result = await lockNode.SetValue(GetUserCodeValue(userCodeId), code); // If the result is not successful log the message if (!result.Success) { Log.Logger.Error("Failed to set user code {@userCodeId} to {@code}. Error message: {message}", userCodeId, code, result.Message); } stopwatch.Stop(); Log.Logger.Debug("Set user code for user code ID: {userCodeId} to {code} in {ElapsedMilliseconds} ms", userCodeId, code, stopwatch.ElapsedMilliseconds); // Return the result return result.Success; } public async Task SetCodeAsAvailable(int userCode) { Stopwatch stopwatch = new(); stopwatch.Start(); // Log the start of the operation Log.Logger.Debug("Setting user code status for user code ID: {userCode} to available", userCode); // Setup the set value task CMDResult result = await lockNode.SetValue(GetUserStatusValue(userCode), (int)UserCodeStatus.AVAILABLE); // If the result is not successful log the message if (!result.Success) { Log.Logger.Error("Failed to set user code {@userCode} to available status. Error message: {message}", userCode, result.Message); } stopwatch.Stop(); Log.Logger.Debug("Set user code status for user code ID: {userCode} to available in {ElapsedMilliseconds} ms", userCode, stopwatch.ElapsedMilliseconds); // Return the result return result.Success; } private static UserCodeStatus GetUserStatusValue(CMDResult result) { // Parse the payload as a JSON object JObject payloadObject = (JObject)result.ResultPayload; // Return the value property return (UserCodeStatus)payloadObject.GetValue("value")!.ToObject(); } private static string GetUserCodeValue(CMDResult result) { // Parse the payload as a JSON object JObject payloadObject = (JObject)result.ResultPayload; // Return the value property return payloadObject.GetValue("value")!.ToString(); } private static ValueID GetUserStatusValue(int userCodeId) { return new ValueID() { commandClass = 99, endpoint = 0, property = "userIdStatus", propertyKey = userCodeId }; } private static ValueID GetUserCodeValue(int userCodeId) { return new ValueID() { commandClass = 99, endpoint = 0, property = "userCode", propertyKey = userCodeId }; } public static string ValidateCode(string newCode) { // The code must be between 4 and 6 digits if (newCode.Length < 4 || newCode.Length > 6) { return "The code must be between 4 and 6 digits."; } // The code must be numeric if (!int.TryParse(newCode, out int _)) { return "The code must be numeric."; } // Otherwise, the code is valid, return an empty string return string.Empty; } public static string ValidateClearCode(int codeId, int homeCodeId) { // If the code is the home code, return an error message if (codeId == homeCodeId) { return "The home code cannot be cleared."; } return string.Empty; } } }