initial commit
All checks were successful
Build and Publish / Build Yale Access Backend (push) Successful in 28s
Build and Publish / Build Yale Access Frontend (push) Successful in 47s
Build and Publish / Push Yale Access Backend Docker Image (push) Successful in 9s
Build and Publish / Push Yale Access Frontend Docker Image (push) Successful in 10s

This commit is contained in:
2025-01-10 08:37:18 +11:00
commit f577617b4d
80 changed files with 10113 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
using YaleAccess.Models;
namespace YaleAccess.Services.Interfaces
{
public interface IYaleAccessor
{
public Task<YaleUserCode> GetCodeInformationAsync(int userCodeId);
public Task<bool> SetUserCode(int userCodeId, string code);
public Task<bool> SetCodeAsAvailable(int userCode);
}
}

View File

@@ -0,0 +1,30 @@
using YaleAccess.Models;
using YaleAccess.Services.Interfaces;
namespace YaleAccess.Services
{
public class MockYaleAccessor : IYaleAccessor
{
private readonly MockYaleData _data;
public MockYaleAccessor(MockYaleData data)
{
_data = data;
}
public Task<YaleUserCode> GetCodeInformationAsync(int userCodeId)
{
return Task.FromResult(_data.GetUserCode(userCodeId));
}
public Task<bool> SetCodeAsAvailable(int userCode)
{
return Task.FromResult(_data.SetUserCodeAsAvailable(userCode));
}
public Task<bool> SetUserCode(int userCodeId, string code)
{
return Task.FromResult(_data.SetUserCode(userCodeId, code));
}
}
}

View File

@@ -0,0 +1,78 @@
using YaleAccess.Models;
namespace YaleAccess.Services
{
public class MockYaleData
{
public List<YaleUserCode> UserCodes { get; set; } = new();
public MockYaleData()
{
// Create the home code
YaleUserCode homeCode = new()
{
Id = 1,
Code = "1234",
IsHome = true,
Status = UserCodeStatus.ENABLED
};
// Create 5 guest codes
List<YaleUserCode> guestCodes = new();
foreach (int code in Enumerable.Range(2, 5))
{
guestCodes.Add(new YaleUserCode()
{
Id = code,
Code = "",
IsHome = false,
Status = UserCodeStatus.AVAILABLE
});
}
// Add the home code to the list
guestCodes.Add(homeCode);
// Set the user codes
UserCodes = guestCodes;
}
public YaleUserCode GetUserCode(int id)
{
return UserCodes.First(x => x.Id == id);
}
public bool SetUserCode(int id, string code)
{
// Get the user code
YaleUserCode userCode = GetUserCode(id);
// Set the code
userCode.Code = code;
// Update code status
userCode.Status = UserCodeStatus.ENABLED;
// Update the user code in the list
UserCodes[UserCodes.IndexOf(userCode)] = userCode;
// Return true to indicate success
return true;
}
public bool SetUserCodeAsAvailable(int id)
{
// Get the user code
YaleUserCode userCode = GetUserCode(id);
// Set the code
userCode.Status = UserCodeStatus.AVAILABLE;
// Update the user code in the list
UserCodes[UserCodes.IndexOf(userCode)] = userCode;
// Return true to indicate success
return true;
}
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.Extensions.Options;
using Serilog;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using YaleAccess.Models.Options;
namespace YaleAccess.Services
{
public class SMSService
{
private readonly TwiloOptions _twiloOptions;
public SMSService(IOptions<TwiloOptions> twiloOptions)
{
_twiloOptions = twiloOptions.Value;
}
public async Task SendCodeViaSMSAsync(string code, string phoneNumber)
{
// Create a Twilio client
TwilioClient.Init(_twiloOptions.AccountSid, _twiloOptions.AuthToken);
// Send the message
var message = await MessageResource.CreateAsync(
body: $"{_twiloOptions.Message} {code}",
from: new Twilio.Types.PhoneNumber(_twiloOptions.FromNumber),
to: new Twilio.Types.PhoneNumber(phoneNumber)
);
// Log the message
Log.Logger.Information("SMS sent to {PhoneNumber} with message SID {MessageSid}.", phoneNumber, message.Sid);
Log.Logger.Debug("SMS message: {MessageBody}", message.Body);
}
}
}

View File

@@ -0,0 +1,195 @@
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using Serilog;
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<ZWaveOptions> zwave, IOptions<DevicesOptions> 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;
// Subscribe to the driver ready event
driver.DriverReady += () =>
{
isReady = true;
};
driver.StartUpError += (message) =>
{
throw new Exception(message);
};
// Wait for the driver to be ready
while (!isReady)
{
Thread.Sleep(100);
}
// Get the lock node from the driver
lockNode = driver.Controller.Nodes.Get(devicesOptions.YaleLockNodeId);
}
public async Task<YaleUserCode> GetCodeInformationAsync(int userCodeId)
{
// Setup the two tasks to get the values we need
CMDResult status = await lockNode.GetValue(GetUserStatusValue(userCodeId));
CMDResult code = await lockNode.GetValue(GetUserCodeValue(userCodeId));
// Covert the result to a YaleUserCode object
return new YaleUserCode()
{
Id = userCodeId,
Code = GetUserCodeValue(code),
Status = GetUserStatusValue(status),
IsHome = false
};
}
public async Task<bool> SetUserCode(int userCodeId, string 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);
}
// Return the result
return result.Success;
}
public async Task<bool> SetCodeAsAvailable(int 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);
}
// 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<int>();
}
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;
}
}
}