using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; 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 readonly ILogger _logger; private Driver? driver = null; private readonly ZWaveNode lockNode = null!; public YaleAccessor(IOptions zwave, IOptions device, ILogger logger) { // 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 += () => { logger.LogInformation("Z-Wave driver started successfully."); isReady = true; }; driver.StartupErrorEvent += (error) => { logger.LogError("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) { logger.LogError("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); _logger = logger; } public async Task GetCodeInformationAsync(int userCodeId) { Stopwatch stopwatch = new(); stopwatch.Start(); // Log the start of the operation _logger.LogDebug("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)); _logger.LogDebug("Retrieved user code status for user code ID: {userCodeId} in {ElapsedMilliseconds} ms", userCodeId, stopwatch.ElapsedMilliseconds); CMDResult code = await lockNode.GetValue(GetUserCodeValue(userCodeId)); _logger.LogDebug("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(); _logger.LogDebug("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 _logger.LogDebug("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) { _logger.LogError("Failed to set user code {@userCodeId} to {@code}. Error message: {message}", userCodeId, code, result.Message); } stopwatch.Stop(); _logger.LogDebug("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 _logger.LogDebug("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) { _logger.LogError("Failed to set user code {@userCode} to available status. Error message: {message}", userCode, result.Message); } stopwatch.Stop(); _logger.LogDebug("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; } } }