Merge branch 'main' of https://git.innov.energy/Innovenergy/git_trunk
This commit is contained in:
commit
fb86792680
|
@ -4,7 +4,6 @@ using InnovEnergy.App.Backend.Database;
|
||||||
using InnovEnergy.App.Backend.Model;
|
using InnovEnergy.App.Backend.Model;
|
||||||
using InnovEnergy.App.Backend.Model.Relations;
|
using InnovEnergy.App.Backend.Model.Relations;
|
||||||
using InnovEnergy.App.Backend.Utils;
|
using InnovEnergy.App.Backend.Utils;
|
||||||
using InnovEnergy.Lib.Utils;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using HttpContextAccessor = Microsoft.AspNetCore.Http.HttpContextAccessor;
|
using HttpContextAccessor = Microsoft.AspNetCore.Http.HttpContextAccessor;
|
||||||
|
|
||||||
|
@ -58,18 +57,26 @@ public class Controller
|
||||||
|
|
||||||
[Returns(HttpStatusCode.OK)]
|
[Returns(HttpStatusCode.OK)]
|
||||||
[Returns(HttpStatusCode.Unauthorized)]
|
[Returns(HttpStatusCode.Unauthorized)]
|
||||||
[HttpPost($"{nameof(UpdateS3Credentials)}")]
|
[HttpGet($"{nameof(GetInstallationS3Key)}")]
|
||||||
public Object UpdateS3Credentials()
|
public Object GetInstallationS3Key(Int64 installationId)
|
||||||
{
|
{
|
||||||
// TODO: S3Credentials should be per session, not per user
|
|
||||||
|
|
||||||
var caller = GetCaller();
|
var caller = GetCaller();
|
||||||
if (caller is null)
|
if (caller is null)
|
||||||
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
using var db = Db.Connect();
|
using var db = Db.Connect();
|
||||||
|
|
||||||
|
var installation = db
|
||||||
|
.GetAllAccessibleInstallations(caller)
|
||||||
|
.FirstOrDefault(i => i.Id == installationId);
|
||||||
|
|
||||||
return db.CreateAndSaveUserS3ApiKey(caller);
|
if(installation == null)
|
||||||
|
{
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = db.GetInstallationS3Key(installationId);
|
||||||
|
return key ?? db.CreateAndSaveInstallationS3ApiKey(installation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -219,24 +226,64 @@ public class Controller
|
||||||
|
|
||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Returns(HttpStatusCode.OK)]
|
||||||
|
[Returns(HttpStatusCode.Unauthorized)]
|
||||||
|
[HttpPost($"{nameof(CreateUser)}/")]
|
||||||
|
public Object CreateUser(User newUser)
|
||||||
|
{
|
||||||
|
var caller = GetCaller();
|
||||||
|
using var db = Db.Connect();
|
||||||
|
if (caller == null || !caller.HasWriteAccess)
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
|
newUser.ParentId = caller.Id;
|
||||||
|
|
||||||
|
return db.CreateUser(newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Returns(HttpStatusCode.OK)]
|
||||||
|
[Returns(HttpStatusCode.Unauthorized)]
|
||||||
|
[HttpPost($"{nameof(CreateInstallation)}/")]
|
||||||
|
public Object CreateInstallation(Installation installation)
|
||||||
|
{
|
||||||
|
var caller = GetCaller();
|
||||||
|
using var db = Db.Connect();
|
||||||
|
if (caller == null || !caller.HasWriteAccess)
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
|
var id = db.CreateInstallation(installation);
|
||||||
|
|
||||||
|
return db.AddToAccessibleInstallations(caller.Id, id);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Returns(HttpStatusCode.OK)]
|
||||||
|
[Returns(HttpStatusCode.Unauthorized)]
|
||||||
|
[HttpPost($"{nameof(CreateFolder)}/")]
|
||||||
|
public Object CreateFolder(Folder folder)
|
||||||
|
{
|
||||||
|
var caller = GetCaller();
|
||||||
|
using var db = Db.Connect();
|
||||||
|
if (caller == null || !caller.HasWriteAccess)
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
|
var id = db.CreateFolder(folder);
|
||||||
|
return db.AddToAccessibleFolders(caller.Id, id);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
[Returns(HttpStatusCode.OK)]
|
[Returns(HttpStatusCode.OK)]
|
||||||
[Returns(HttpStatusCode.Unauthorized)]
|
[Returns(HttpStatusCode.Unauthorized)]
|
||||||
[HttpPut($"{nameof(UpdateUser)}/")]
|
[HttpPut($"{nameof(UpdateUser)}/")]
|
||||||
public Object UpdateUser(User updatedUser)
|
public Object UpdateUser(User updatedUser)
|
||||||
{
|
{
|
||||||
// TODO: distinguish between create and update
|
|
||||||
|
|
||||||
var caller = GetCaller();
|
var caller = GetCaller();
|
||||||
if (caller == null)
|
using var db = Db.Connect();
|
||||||
|
if (caller == null || !db.IsParentOfChild(caller.Id, updatedUser) || !caller.HasWriteAccess)
|
||||||
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
using var db = Db.Connect();
|
return db.UpdateUser(updatedUser);
|
||||||
|
|
||||||
return db.GetUserById(updatedUser.Id) != null
|
|
||||||
? db.UpdateUser(updatedUser)
|
|
||||||
: db.CreateUser(updatedUser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -252,15 +299,21 @@ public class Controller
|
||||||
|
|
||||||
using var db = Db.Connect();
|
using var db = Db.Connect();
|
||||||
|
|
||||||
var hasAccessToInstallation = db
|
var installationFromAccessibleInstallations = db
|
||||||
.GetAllAccessibleInstallations(caller)
|
.GetAllAccessibleInstallations(caller)
|
||||||
.Any(i => i.Id == installation.Id);
|
.FirstOrDefault(i => i.Id == installation.Id);
|
||||||
|
|
||||||
if (!hasAccessToInstallation)
|
if (installationFromAccessibleInstallations == null)
|
||||||
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
// TODO: accessibility by other users etc
|
// TODO: accessibility by other users etc
|
||||||
// TODO: sanity check changes
|
// TODO: sanity check changes
|
||||||
|
// foreach(var property in installationFromAccessibleInstallations.GetType().GetProperties()){
|
||||||
|
// if(installation.GetType().GetProperties().Contains(property))
|
||||||
|
// {
|
||||||
|
// property.SetValue(installationFromAccessibleInstallations, property.GetValue(installation));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
return db.UpdateInstallation(installation);
|
return db.UpdateInstallation(installation);
|
||||||
}
|
}
|
||||||
|
@ -278,17 +331,24 @@ public class Controller
|
||||||
|
|
||||||
using var db = Db.Connect();
|
using var db = Db.Connect();
|
||||||
|
|
||||||
var hasAccessToFolder = db
|
var installationFromAccessibleFolders = db
|
||||||
.GetAllAccessibleFolders(caller)
|
.GetAllAccessibleFolders(caller)
|
||||||
.Any(f => f.Id == folder.Id);
|
.FirstOrDefault(f => f.Id == folder.Id);
|
||||||
|
|
||||||
if (!hasAccessToFolder)
|
if (installationFromAccessibleFolders == null)
|
||||||
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
// TODO: accessibility by other users etc
|
// TODO: accessibility by other users etc
|
||||||
// TODO: sanity check changes
|
// TODO: sanity check changes
|
||||||
|
|
||||||
return db.UpdateFolder(folder);
|
// foreach(var property in installationFromAccessibleFolders.GetType().GetProperties()){
|
||||||
|
// if(folder.GetType().GetProperties().Contains(property))
|
||||||
|
// {
|
||||||
|
// property.SetValue(installationFromAccessibleFolders, property.GetValue(folder));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return db.UpdateFolder(installationFromAccessibleFolders);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Returns(HttpStatusCode.OK)]
|
[Returns(HttpStatusCode.OK)]
|
||||||
|
|
|
@ -10,16 +10,16 @@ namespace InnovEnergy.App.Backend.Database;
|
||||||
public partial class Db : IDisposable
|
public partial class Db : IDisposable
|
||||||
{
|
{
|
||||||
internal const String DbPath = "./db.sqlite";
|
internal const String DbPath = "./db.sqlite";
|
||||||
|
|
||||||
private readonly SQLiteConnection _Db; // internal handle to the connection, disposable
|
private readonly SQLiteConnection _Db; // internal handle to the connection, disposable
|
||||||
|
|
||||||
private TableQuery<Session> Sessions => _Db.Table<Session>();
|
private TableQuery<Session> Sessions => _Db.Table<Session>();
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "AccessToDisposedClosure")]
|
[SuppressMessage("ReSharper", "AccessToDisposedClosure")]
|
||||||
static Db()
|
static Db()
|
||||||
{
|
{
|
||||||
// on startup create/migrate tables
|
// on startup create/migrate tables
|
||||||
|
|
||||||
using var db = new SQLiteConnection(DbPath);
|
using var db = new SQLiteConnection(DbPath);
|
||||||
|
|
||||||
db.RunInTransaction(() =>
|
db.RunInTransaction(() =>
|
||||||
|
@ -31,105 +31,115 @@ public partial class Db : IDisposable
|
||||||
db.CreateTable<User2Installation>();
|
db.CreateTable<User2Installation>();
|
||||||
db.CreateTable<Session>();
|
db.CreateTable<Session>();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// private, force access through Connect()
|
// private, force access through Connect()
|
||||||
private Db() => _Db = new SQLiteConnection(DbPath);
|
private Db() => _Db = new SQLiteConnection(DbPath);
|
||||||
|
|
||||||
public static Db Connect() => new Db();
|
public static Db Connect() => new Db();
|
||||||
|
|
||||||
public void Dispose() => _Db.Dispose();
|
public void Dispose() => _Db.Dispose();
|
||||||
|
|
||||||
|
|
||||||
// the C in CRUD
|
// the C in CRUD
|
||||||
private Result Create(TreeNode treeNode)
|
private Int64 Create(TreeNode treeNode)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_Db.Insert(treeNode);
|
_Db.Insert(treeNode);
|
||||||
|
return SQLite3.LastInsertRowid(_Db.Handle);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return Result.Error(e);
|
return -1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Boolean Create(Session session)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_Db.Insert(session);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Result.Ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the U in CRUD
|
// the U in CRUD
|
||||||
private Result Update(TreeNode treeNode)
|
private Boolean Update(TreeNode treeNode)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_Db.InsertOrReplace(treeNode);
|
_Db.InsertOrReplace(treeNode);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return Result.Error(e);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.Ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the D in CRUD
|
// the D in CRUD
|
||||||
private Result Delete(TreeNode treeNode)
|
private Boolean Delete(TreeNode treeNode)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_Db.Delete(treeNode);
|
_Db.Delete(treeNode);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return Result.Error(e);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.Ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Installation> GetAllAccessibleInstallations(User user)
|
public IEnumerable<Installation> GetAllAccessibleInstallations(User user)
|
||||||
{
|
{
|
||||||
var direct = GetDirectlyAccessibleInstallations(user);
|
var direct = GetDirectlyAccessibleInstallations(user);
|
||||||
var fromFolders = GetAllAccessibleFolders(user)
|
var fromFolders = GetAllAccessibleFolders(user)
|
||||||
.SelectMany(GetChildInstallations);
|
.SelectMany(GetChildInstallations);
|
||||||
|
|
||||||
return direct
|
return direct
|
||||||
.Concat(fromFolders)
|
.Concat(fromFolders)
|
||||||
.Distinct();
|
.Distinct();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Folder> GetAllAccessibleFolders(User user)
|
public IEnumerable<Folder> GetAllAccessibleFolders(User user)
|
||||||
{
|
{
|
||||||
return GetDirectlyAccessibleFolders(user)
|
return GetDirectlyAccessibleFolders(user)
|
||||||
.SelectMany(GetDescendantFolders)
|
.SelectMany(GetDescendantFolders)
|
||||||
.Distinct();
|
.Distinct();
|
||||||
|
|
||||||
// Distinct because the user might have direct access
|
// Distinct because the user might have direct access
|
||||||
// to a child folder of a folder he has already access to
|
// to a child folder of a folder he has already access to
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public IEnumerable<Installation> GetDirectlyAccessibleInstallations(User user)
|
public IEnumerable<Installation> GetDirectlyAccessibleInstallations(User user)
|
||||||
{
|
{
|
||||||
return User2Installation
|
return User2Installation
|
||||||
.Where(r => r.UserId == user.Id)
|
.Where(r => r.UserId == user.Id)
|
||||||
.Select(r => r.InstallationId)
|
.Select(r => r.InstallationId)
|
||||||
.Select(GetInstallationById)
|
.Select(GetInstallationById)
|
||||||
.NotNull()
|
.NotNull()
|
||||||
.Do(i => i.ParentId = 0); // hide inaccessible parents from calling user
|
.Do(i => i.ParentId = 0); // hide inaccessible parents from calling user
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Folder> GetDirectlyAccessibleFolders(User user)
|
public IEnumerable<Folder> GetDirectlyAccessibleFolders(User user)
|
||||||
{
|
{
|
||||||
return User2Folder
|
return User2Folder
|
||||||
.Where(r => r.UserId == user.Id)
|
.Where(r => r.UserId == user.Id)
|
||||||
.Select(r => r.FolderId)
|
.Select(r => r.FolderId)
|
||||||
.Select(GetFolderById)
|
.Select(GetFolderById)
|
||||||
.NotNull()
|
.NotNull()
|
||||||
.Do(i => i.ParentId = 0); // hide inaccessible parents from calling user;
|
.Do(i => i.ParentId = 0); // hide inaccessible parents from calling user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result AddToAccessibleInstallations(Int64 userId, Int64 updatedInstallationId)
|
public Boolean AddToAccessibleInstallations(Int64 userId, Int64 updatedInstallationId)
|
||||||
{
|
{
|
||||||
var con = new User2Installation
|
var con = new User2Installation
|
||||||
{
|
{
|
||||||
|
@ -139,17 +149,16 @@ public partial class Db : IDisposable
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_Db.InsertOrReplace(con);
|
_Db.Insert(con);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return Result.Error(e);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.Ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result AddToAccessibleFolders(Int64 userId, Int64 updatedFolderId)
|
public Boolean AddToAccessibleFolders(Int64 userId, Int64 updatedFolderId)
|
||||||
{
|
{
|
||||||
var con = new User2Folder
|
var con = new User2Folder
|
||||||
{
|
{
|
||||||
|
@ -159,16 +168,14 @@ public partial class Db : IDisposable
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_Db.InsertOrReplace(con);
|
_Db.Insert(con);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return Result.Error(e);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.Ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public User? GetUserByToken(String token)
|
public User? GetUserByToken(String token)
|
||||||
|
@ -182,33 +189,33 @@ public partial class Db : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Result NewSession(Session ses)
|
public Boolean NewSession(Session ses) => Create(ses);
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_Db.InsertOrReplace(ses);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Result.Error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.Ok;
|
public Boolean DeleteSession(Int64 id)
|
||||||
}
|
|
||||||
|
|
||||||
public Result DeleteSession(Int64 id)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Sessions.Delete(u => u.UserId == id);
|
Sessions.Delete(u => u.UserId == id);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return Result.Error(e);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.Ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public Object? GetInstallationS3Key(Int64 installationId)
|
||||||
|
{
|
||||||
|
return Installations
|
||||||
|
.FirstOrDefault(installation => installation.Id == installationId).S3Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteS3KeysDaily()
|
||||||
|
{
|
||||||
|
foreach (var installation in Installations.ToList())
|
||||||
|
{
|
||||||
|
installation.S3Key = null;
|
||||||
|
Update(installation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
using InnovEnergy.App.Backend.Model;
|
||||||
|
using SQLite;
|
||||||
|
|
||||||
|
namespace InnovEnergy.App.Backend.Database;
|
||||||
|
|
||||||
|
|
||||||
|
// TODO ?
|
||||||
|
public struct DbConnection
|
||||||
|
{
|
||||||
|
public DbConnection(SQLiteConnection connection, User caller)
|
||||||
|
{
|
||||||
|
Connection = connection;
|
||||||
|
Caller = caller;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SQLiteConnection Connection { get;}
|
||||||
|
public User Caller { get;}
|
||||||
|
}
|
|
@ -26,7 +26,6 @@ public partial class Db
|
||||||
return Folders.Where(f => f.ParentId == parent.Id);
|
return Folders.Where(f => f.ParentId == parent.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public IEnumerable<Folder> GetDescendantFolders(Folder parent)
|
public IEnumerable<Folder> GetDescendantFolders(Folder parent)
|
||||||
{
|
{
|
||||||
return parent.Traverse(GetChildFolders);
|
return parent.Traverse(GetChildFolders);
|
||||||
|
@ -47,31 +46,31 @@ public partial class Db
|
||||||
return parent.Traverse(GetChildUsers);
|
return parent.Traverse(GetChildUsers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result CreateFolder(Folder folder)
|
public Int64 CreateFolder(Folder folder)
|
||||||
{
|
{
|
||||||
return Create(folder);
|
return Create(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result UpdateFolder(Folder folder)
|
public Boolean UpdateFolder(Folder folder)
|
||||||
{
|
{
|
||||||
// TODO: no circles in path
|
// TODO: no circles in path
|
||||||
|
|
||||||
return Update(folder);
|
return Update(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result ChangeParent(Installation child, Int64 parentId)
|
public Boolean ChangeParent(Installation child, Int64 parentId)
|
||||||
{
|
{
|
||||||
child.ParentId = parentId;
|
child.ParentId = parentId;
|
||||||
return UpdateInstallation(child);
|
return UpdateInstallation(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result ChangeParent(Folder child, Int64 parentId)
|
public Boolean ChangeParent(Folder child, Int64 parentId)
|
||||||
{
|
{
|
||||||
child.ParentId = parentId;
|
child.ParentId = parentId;
|
||||||
return UpdateFolder(child);
|
return UpdateFolder(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result DeleteFolder(Folder folder)
|
public Boolean DeleteFolder(Folder folder)
|
||||||
{
|
{
|
||||||
// Delete direct children
|
// Delete direct children
|
||||||
User2Folder .Delete(f => f.FolderId == folder.Id);
|
User2Folder .Delete(f => f.FolderId == folder.Id);
|
||||||
|
|
|
@ -14,22 +14,24 @@ public partial class Db
|
||||||
public Installation? GetInstallationById(Int64 id) => Installations
|
public Installation? GetInstallationById(Int64 id) => Installations
|
||||||
.FirstOrDefault(u => u.Id == id);
|
.FirstOrDefault(u => u.Id == id);
|
||||||
|
|
||||||
public Result CreateInstallation(Installation installation)
|
public Int64 CreateInstallation(Installation installation)
|
||||||
{
|
{
|
||||||
return Create(installation);
|
return Create(installation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result UpdateInstallation(Installation installation)
|
public Boolean UpdateInstallation(Installation installation)
|
||||||
{
|
{
|
||||||
return Update(installation);
|
return Update(installation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Result DeleteInstallation(Installation installation)
|
public Boolean DeleteInstallation(Installation installation)
|
||||||
{
|
{
|
||||||
User2Installation.Delete(i => i.InstallationId == installation.Id);
|
User2Installation.Delete(i => i.InstallationId == installation.Id);
|
||||||
|
|
||||||
return Delete(installation);
|
return Delete(installation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Mail;
|
using System.Net.Mail;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using Flurl.Http;
|
using System.Text.RegularExpressions;
|
||||||
using InnovEnergy.App.Backend.Model;
|
using InnovEnergy.App.Backend.Model;
|
||||||
using InnovEnergy.App.Backend.Utils;
|
using InnovEnergy.App.Backend.Utils;
|
||||||
using InnovEnergy.Lib.Utils;
|
using InnovEnergy.Lib.Utils;
|
||||||
|
@ -25,27 +25,35 @@ public partial class Db
|
||||||
return Users.FirstOrDefault(u => u.Id == id);
|
return Users.FirstOrDefault(u => u.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean IsParentOfChild(User parent, User child)
|
public Boolean IsParentOfChild(Int64 parentId, User child)
|
||||||
{
|
{
|
||||||
var parentPointer = child.ParentId;
|
return Ancestors(child)
|
||||||
|
.Any(u => u.Id == parentId);
|
||||||
|
}
|
||||||
|
|
||||||
if (parent.Id == child.Id)
|
private IEnumerable<User> Ancestors(User child)
|
||||||
return true;
|
{
|
||||||
|
return child.Unfold(GetParent);
|
||||||
|
}
|
||||||
|
|
||||||
while (parentPointer != null && parentPointer != parent.Id)
|
public User? GetParent(User u)
|
||||||
{
|
{
|
||||||
parentPointer = GetUserById(parentPointer).ParentId;
|
return IsRoot(u)
|
||||||
}
|
? null
|
||||||
|
: GetUserById(u.ParentId);
|
||||||
|
}
|
||||||
|
|
||||||
return parentPointer == parent.Id;
|
public static Boolean IsRoot(User u)
|
||||||
|
{
|
||||||
|
return u.ParentId == 0; // root has ParentId 0 by definition
|
||||||
}
|
}
|
||||||
|
|
||||||
public User? GetUserByEmail(String email) => Users.FirstOrDefault(u => u.Email == email);
|
public User? GetUserByEmail(String email) => Users.FirstOrDefault(u => u.Email == email);
|
||||||
|
|
||||||
public Result CreateUser(User user)
|
public Int64 CreateUser(User user)
|
||||||
{
|
{
|
||||||
if (GetUserByEmail(user.Email) is not null)
|
if (GetUserByEmail(user.Email) is not null)
|
||||||
return Result.Error("User with that email already exists");
|
return -1; // TODO: User with that email already exists
|
||||||
|
|
||||||
//Salting and Hashing password
|
//Salting and Hashing password
|
||||||
var salt = Crypto.GenerateSalt();
|
var salt = Crypto.GenerateSalt();
|
||||||
|
@ -58,73 +66,149 @@ public partial class Db
|
||||||
return Create(user);
|
return Create(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
|
||||||
public Object CreateAndSaveUserS3ApiKey(User user)
|
private static Byte[] HmacSha256Digest(String message, String secret)
|
||||||
|
{
|
||||||
|
var encoding = new UTF8Encoding();
|
||||||
|
var keyBytes = encoding.GetBytes(secret);
|
||||||
|
var messageBytes = encoding.GetBytes(message);
|
||||||
|
var cryptographer = new HMACSHA256(keyBytes);
|
||||||
|
|
||||||
|
var bytes = cryptographer.ComputeHash(messageBytes);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String BuildSignature(String method, String path, String data, Int64 time, String secret)
|
||||||
|
{
|
||||||
|
var messageToSign = "";
|
||||||
|
messageToSign += method + " /v2/" + path + "\n";
|
||||||
|
messageToSign += data + "\n";
|
||||||
|
|
||||||
|
// query strings
|
||||||
|
messageToSign += "\n";
|
||||||
|
// headers
|
||||||
|
messageToSign += "\n";
|
||||||
|
|
||||||
|
messageToSign += time;
|
||||||
|
|
||||||
|
Console.WriteLine("Message to sign:\n" + messageToSign);
|
||||||
|
|
||||||
|
|
||||||
|
var hmac = HmacSha256Digest(messageToSign, secret);
|
||||||
|
return Convert.ToBase64String(hmac);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public Object CreateAndSaveUserS3ApiKey(User user)
|
||||||
|
// {
|
||||||
|
// //EXOSCALE API URL
|
||||||
|
// const String url = "https://api-ch-dk-2.exoscale.com/v2/";
|
||||||
|
// const String path = "access-key";
|
||||||
|
//
|
||||||
|
// //TODO HIDE ME
|
||||||
|
// const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0";
|
||||||
|
// const String apiKey = "EXOb98ec9008e3ec16e19d7b593";
|
||||||
|
//
|
||||||
|
// var installationList = User2Installation
|
||||||
|
// .Where(i => i.UserId == user.Id)
|
||||||
|
// .SelectMany(i => Installations.Where(f => i.InstallationId == f.Id))
|
||||||
|
// .ToList();
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// var instList = new JsonArray();
|
||||||
|
//
|
||||||
|
// foreach (var installation in installationList)
|
||||||
|
// {
|
||||||
|
// instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"});
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var jsonPayload = new JsonObject { ["name"] = user.Email, ["operations"] = new JsonArray{ "list-sos-bucket", "get-sos-object" }, ["content"] = instList};
|
||||||
|
// var stringPayload = jsonPayload.ToJsonString();
|
||||||
|
//
|
||||||
|
// var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
|
||||||
|
// var signature = BuildSignature("POST", path, stringPayload, unixExpiration , secret);
|
||||||
|
//
|
||||||
|
// var authHeader = "credential="+apiKey+",expires="+unixExpiration+",signature="+signature;
|
||||||
|
//
|
||||||
|
// var client = new HttpClient();
|
||||||
|
// client.DefaultRequestHeaders.Authorization =
|
||||||
|
// new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader);
|
||||||
|
//
|
||||||
|
// var content = new StringContent(stringPayload, Encoding.UTF8, "application/json");
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// var response = client.PostAsync(url+path, content).Result;
|
||||||
|
//
|
||||||
|
// if (response.StatusCode.ToString() != "OK")
|
||||||
|
// {
|
||||||
|
// return response;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var responseString = response.Content.ReadAsStringAsync().Result;
|
||||||
|
// return Enumerable.Last(Regex.Match(responseString, "key\\\":\\\"([A-Z])\\w+").ToString().Split('"'));
|
||||||
|
// // return SetUserS3ApiKey(user, newKey);
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
public Object? CreateAndSaveInstallationS3ApiKey(Installation installation)
|
||||||
{
|
{
|
||||||
//EXOSCALE API URL
|
//EXOSCALE API URL
|
||||||
const String url = "https://api-ch-dk-2.exoscale.com/v2/access-key";
|
const String url = "https://api-ch-dk-2.exoscale.com/v2/";
|
||||||
|
const String path = "access-key";
|
||||||
|
|
||||||
|
//TODO HIDE ME
|
||||||
const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0";
|
const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0";
|
||||||
const String apiKey = "EXOb98ec9008e3ec16e19d7b593";
|
const String apiKey = "EXOb98ec9008e3ec16e19d7b593";
|
||||||
|
|
||||||
var installationList = User2Installation
|
|
||||||
.Where(i => i.UserId == user.Id)
|
|
||||||
.SelectMany(i => Installations.Where(f => i.InstallationId == f.Id))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
|
|
||||||
var instList = new JsonArray();
|
var instList = new JsonArray();
|
||||||
|
instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"});
|
||||||
|
|
||||||
|
var jsonPayload = new JsonObject { ["name"] = installation.Id, ["operations"] = new JsonArray{ "list-sos-bucket", "get-sos-object" }, ["content"] = instList};
|
||||||
|
var stringPayload = jsonPayload.ToJsonString();
|
||||||
|
|
||||||
foreach (var installation in installationList)
|
var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
|
||||||
|
var signature = BuildSignature("POST", path, stringPayload, unixExpiration , secret);
|
||||||
|
|
||||||
|
var authHeader = "credential="+apiKey+",expires="+unixExpiration+",signature="+signature;
|
||||||
|
|
||||||
|
var client = new HttpClient();
|
||||||
|
client.DefaultRequestHeaders.Authorization =
|
||||||
|
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader);
|
||||||
|
|
||||||
|
var content = new StringContent(stringPayload, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
|
||||||
|
var response = client.PostAsync(url+path, content).Result;
|
||||||
|
|
||||||
|
if (response.StatusCode.ToString() != "OK")
|
||||||
{
|
{
|
||||||
instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"});
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonPayload = new JsonObject { ["name"] = user.Email, ["operations"] = new JsonArray{ "getObject", "listBucket" }, ["content"] = instList};
|
|
||||||
var expiration = DateTime.Now.AddSeconds(60);
|
|
||||||
|
|
||||||
var signature = $"POST /v2/access-key\n{jsonPayload}\n\n\n{((DateTimeOffset)expiration).ToUnixTimeSeconds()}";
|
|
||||||
using var hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
|
||||||
|
|
||||||
signature = Encoding.UTF8
|
|
||||||
.GetBytes(signature)
|
|
||||||
.Apply(hmacSha256.ComputeHash)
|
|
||||||
.Apply(Convert.ToBase64String);
|
|
||||||
|
|
||||||
var keyJson = url
|
var responseString = response.Content.ReadAsStringAsync().Result;
|
||||||
.WithHeader("Authorization",
|
var newKey = Enumerable.Last(Regex.Match(responseString, "key\\\":\\\"([A-Z])\\w+").ToString().Split('"'));
|
||||||
$"EXO2-HMAC-SHA256 credential={apiKey},expires={((DateTimeOffset)expiration).ToUnixTimeSeconds()},signature={signature}");
|
|
||||||
|
installation.S3Key = newKey;
|
||||||
|
UpdateInstallation(installation);
|
||||||
var result = keyJson.PostJsonAsync(jsonPayload.ToString())
|
return newKey;
|
||||||
.ReceiveJson()
|
|
||||||
.Result;
|
|
||||||
return result;
|
|
||||||
// return SetUserS3ApiKey(user, keyJson.GetValue("key"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result SetUserS3ApiKey(User user, String key)
|
public Boolean UpdateUser(User user)
|
||||||
{
|
|
||||||
user.S3Key = key;
|
|
||||||
return Update(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Result UpdateUser(User user)
|
|
||||||
{
|
{
|
||||||
var oldUser = GetUserById(user.Id);
|
var oldUser = GetUserById(user.Id);
|
||||||
if (oldUser == null)
|
if (oldUser == null)
|
||||||
return Result.Error("User doesn't exist");
|
return false; // TODO: "User doesn't exist"
|
||||||
|
|
||||||
//Checking for unchangeable things
|
//Checking for unchangeable things
|
||||||
// TODO: depends on privileges of caller
|
// TODO: depends on privileges of caller
|
||||||
|
|
||||||
user.Id = oldUser.Id;
|
user.Id = oldUser.Id;
|
||||||
user.ParentId = oldUser.ParentId;
|
user.ParentId = oldUser.ParentId;
|
||||||
user.Email = oldUser.Email;
|
user.Email = oldUser.Email;
|
||||||
|
|
||||||
return Update(user);
|
return Update(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result DeleteUser(User user)
|
public Boolean DeleteUser(User user)
|
||||||
{
|
{
|
||||||
User2Folder.Delete(u => u.UserId == user.Id);
|
User2Folder.Delete(u => u.UserId == user.Id);
|
||||||
User2Installation.Delete(u => u.UserId == user.Id);
|
User2Installation.Delete(u => u.UserId == user.Id);
|
||||||
|
|
|
@ -14,6 +14,7 @@ public class Installation : TreeNode
|
||||||
public Double Long { get; set; }
|
public Double Long { get; set; }
|
||||||
|
|
||||||
public String S3Bucket { get; set; } = "";
|
public String S3Bucket { get; set; } = "";
|
||||||
|
public String? S3Key { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,10 @@ namespace InnovEnergy.App.Backend.Model;
|
||||||
public class User : TreeNode
|
public class User : TreeNode
|
||||||
{
|
{
|
||||||
[Indexed]
|
[Indexed]
|
||||||
public String Email { get; set; } = "";
|
public String Email { get; set; } = null!;
|
||||||
public Boolean HasWriteAccess { get; set; }
|
public Boolean HasWriteAccess { get; set; } = false;
|
||||||
public String S3Key { get; set; }
|
public String Salt { get; set; } = null!;
|
||||||
public String Salt { get; set; }
|
public String Password { get; set; } = null!;
|
||||||
public String Password { get; set; }
|
|
||||||
|
|
||||||
// TODO: must reset pwd
|
// TODO: must reset pwd
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Reactive.Linq;
|
||||||
using InnovEnergy.App.Backend.Database;
|
using InnovEnergy.App.Backend.Database;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
|
@ -10,11 +11,13 @@ public static class Program
|
||||||
using (var db = Db.Connect())
|
using (var db = Db.Connect())
|
||||||
db.CreateFakeRelations();
|
db.CreateFakeRelations();
|
||||||
|
|
||||||
|
Observable.Interval(TimeSpan.FromDays(1)).Subscribe((_) => deleteInstallationS3KeysDaily());
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddControllers(); // TODO: remove magic, specify controllers explicitly
|
builder.Services.AddControllers(); // TODO: remove magic, specify controllers explicitly
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.WithOrigins("*").AllowAnyHeader().AllowAnyMethod()));
|
builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.WithOrigins("*").AllowAnyHeader().AllowAnyMethod()));
|
||||||
|
@ -34,7 +37,7 @@ public static class Program
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI(cfg => cfg.EnableFilter());
|
app.UseSwaggerUI(cfg => cfg.EnableFilter());
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
@ -44,6 +47,13 @@ public static class Program
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void deleteInstallationS3KeysDaily()
|
||||||
|
{
|
||||||
|
using var db = Db.Connect();
|
||||||
|
db.DeleteS3KeysDaily();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task SetSessionUser(HttpContext ctx, RequestDelegate next)
|
private static async Task SetSessionUser(HttpContext ctx, RequestDelegate next)
|
||||||
{
|
{
|
||||||
var headers = ctx.Request.Headers;
|
var headers = ctx.Request.Headers;
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
namespace InnovEnergy.App.Backend.Utils;
|
|
||||||
|
|
||||||
public class Result
|
|
||||||
{
|
|
||||||
private const String OkMsg = "Ok";
|
|
||||||
|
|
||||||
private readonly String _Error;
|
|
||||||
|
|
||||||
public static Result Ok = new Result(OkMsg);
|
|
||||||
|
|
||||||
public Boolean IsError => _Error != OkMsg;
|
|
||||||
public Boolean IsSuccess => _Error == OkMsg;
|
|
||||||
|
|
||||||
private Result(String error) => _Error = error;
|
|
||||||
|
|
||||||
public static Result Error(String error) => new Result(error);
|
|
||||||
|
|
||||||
public static Result Error(Exception exception)
|
|
||||||
{
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
var msg = exception.ToString(); // includes stacktrace
|
|
||||||
#else
|
|
||||||
var msg = exception.Message; // excludes stacktrace
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return new Result(msg);
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
|
@ -20,32 +20,32 @@ public static class Config
|
||||||
|
|
||||||
public static readonly IReadOnlyList<Signal> Signals = new Signal[]
|
public static readonly IReadOnlyList<Signal> Signals = new Signal[]
|
||||||
{
|
{
|
||||||
new(s => s.CurrentL1, "/Ac/L1/Current", "0.0 A"),
|
new(s => s.Ac.L1.Current, "/Ac/L1/Current", "0.0 A"),
|
||||||
new(s => s.CurrentL2, "/Ac/L2/Current", "0.0 A"),
|
new(s => s.Ac.L2.Current, "/Ac/L2/Current", "0.0 A"),
|
||||||
new(s => s.CurrentL3, "/Ac/L3/Current", "0.0 A"),
|
new(s => s.Ac.L3.Current, "/Ac/L3/Current", "0.0 A"),
|
||||||
new(s => s.CurrentL123, "/Ac/Current", "0.0 A"),
|
new(s => s.Ac.L1.Current + s.Ac.L2.Current + s.Ac.L3.Current, "/Ac/Current", "0.0 A"),
|
||||||
|
|
||||||
new(s => s.VoltageL1N, "/Ac/L1/Voltage", "0.0 A"),
|
new(s => s.Ac.L1.Voltage, "/Ac/L1/Voltage", "0.0 A"),
|
||||||
new(s => s.VoltageL2N, "/Ac/L2/Voltage", "0.0 A"),
|
new(s => s.Ac.L2.Voltage, "/Ac/L2/Voltage", "0.0 A"),
|
||||||
new(s => s.VoltageL3N, "/Ac/L3/Voltage", "0.0 A"),
|
new(s => s.Ac.L3.Voltage, "/Ac/L3/Voltage", "0.0 A"),
|
||||||
new(s => (s.VoltageL1N + s.VoltageL2N + s.VoltageL3N) / 3.0m, "/Ac/Voltage", "0.0 A"),
|
new(s => (s.Ac.L1.Voltage + s.Ac.L2.Voltage + s.Ac.L3.Voltage) / 3.0m, "/Ac/Voltage", "0.0 A"),
|
||||||
|
|
||||||
new(s => s.ActivePowerL1, "/Ac/L1/Power", "0 W"),
|
new(s => s.Ac.L1.ActivePower, "/Ac/L1/Power", "0 W"),
|
||||||
new(s => s.ActivePowerL2, "/Ac/L2/Power", "0 W"),
|
new(s => s.Ac.L2.ActivePower, "/Ac/L2/Power", "0 W"),
|
||||||
new(s => s.ActivePowerL3, "/Ac/L3/Power", "0 W"),
|
new(s => s.Ac.L3.ActivePower, "/Ac/L3/Power", "0 W"),
|
||||||
new(s => s.ActivePowerL123, "/Ac/Power", "0 W"),
|
new(s => s.Ac.ActivePower, "/Ac/Power", "0 W"),
|
||||||
|
|
||||||
new(s => s.EnergyImportL123, "Ac/Energy/Forward", "0.00 kWh"),
|
// new(s => s.EnergyImportL123, "Ac/Energy/Forward", "0.00 kWh"),
|
||||||
new(s => s.EnergyExportL123, "Ac/Energy/Reverse", "0.00 kWh"),
|
// new(s => s.EnergyExportL123, "Ac/Energy/Reverse", "0.00 kWh"),
|
||||||
|
//
|
||||||
new(s => s.EnergyImportL1, "Ac/L1/Energy/Forward", "0.00 kWh"),
|
// new(s => s.EnergyImportL1, "Ac/L1/Energy/Forward", "0.00 kWh"),
|
||||||
new(s => s.EnergyExportL1, "Ac/L1/Energy/Reverse", "0.00 kWh"),
|
// new(s => s.EnergyExportL1, "Ac/L1/Energy/Reverse", "0.00 kWh"),
|
||||||
|
//
|
||||||
new(s => s.EnergyImportL2, "Ac/L2/Energy/Forward", "0.00 kWh"),
|
// new(s => s.EnergyImportL2, "Ac/L2/Energy/Forward", "0.00 kWh"),
|
||||||
new(s => s.EnergyExportL2, "Ac/L2/Energy/Reverse", "0.00 kWh"),
|
// new(s => s.EnergyExportL2, "Ac/L2/Energy/Reverse", "0.00 kWh"),
|
||||||
|
//
|
||||||
new(s => s.EnergyImportL3, "Ac/L3/Energy/Forward", "0.00 kWh"),
|
// new(s => s.EnergyImportL3, "Ac/L3/Energy/Forward", "0.00 kWh"),
|
||||||
new(s => s.EnergyExportL3, "Ac/L3/Energy/Reverse", "0.00 kWh"),
|
// new(s => s.EnergyExportL3, "Ac/L3/Energy/Reverse", "0.00 kWh"),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static VeProperties DefaultProperties => new VeProperties
|
public static VeProperties DefaultProperties => new VeProperties
|
||||||
|
|
|
@ -1,46 +1,72 @@
|
||||||
using DecimalMath;
|
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||||
using InnovEnergy.Lib.Protocols.Modbus.Connections;
|
using InnovEnergy.Lib.Protocols.Modbus.Connections;
|
||||||
using InnovEnergy.Lib.StatusApi.Connections;
|
using InnovEnergy.Lib.Units.Composite;
|
||||||
|
using static DecimalMath.DecimalEx;
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.AMPT;
|
namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
|
||||||
public class AmptCommunicationUnit
|
public class AmptCommunicationUnit
|
||||||
{
|
{
|
||||||
private ModbusTcpClient Modbus { get; }
|
private ModbusTcpClient? Modbus { get; set; }
|
||||||
|
|
||||||
private const Int32 RegistersPerDevice = 16;
|
private const UInt16 RegistersPerDevice = 16;
|
||||||
private const Int32 FirstDeviceOffset = 85;
|
private const UInt16 FirstDeviceOffset = 85;
|
||||||
|
|
||||||
|
public String Hostname { get; }
|
||||||
|
public UInt16 Port { get; }
|
||||||
|
public Byte SlaveAddress { get; }
|
||||||
|
|
||||||
|
|
||||||
public AmptCommunicationUnit(String hostname, UInt16 port = 502, Byte slaveAddress = 1)
|
public AmptCommunicationUnit(String hostname, UInt16 port = 502, Byte slaveAddress = 1)
|
||||||
{
|
{
|
||||||
var connection = new ModbusTcpConnection(hostname, port);
|
Hostname = hostname;
|
||||||
Modbus = new ModbusTcpClient(connection, slaveAddress);
|
Port = port;
|
||||||
|
SlaveAddress = slaveAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AmptStatus? ReadStatus()
|
public AmptCommunicationUnitStatus? ReadStatus()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
OpenConnection();
|
||||||
return TryReadStatus();
|
return TryReadStatus();
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch
|
||||||
{
|
{
|
||||||
Modbus.CloseConnection();
|
CloseConnection();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AmptStatus TryReadStatus()
|
|
||||||
{
|
|
||||||
// Console.WriteLine("Reading Ampt Device");
|
|
||||||
|
|
||||||
var r = Modbus.ReadHoldingRegisters(1, 116);
|
|
||||||
|
|
||||||
var currentFactor = DecimalEx.Pow(10.0m, r.GetInt16(73));
|
private void CloseConnection()
|
||||||
var voltageFactor = DecimalEx.Pow(10.0m, r.GetInt16(74));
|
{
|
||||||
var energyFactor = DecimalEx.Pow(10.0m, r.GetInt16(76) + 3); // +3 => converted from Wh to kWh
|
try
|
||||||
|
{
|
||||||
|
Modbus?.CloseConnection();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
Modbus = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenConnection()
|
||||||
|
{
|
||||||
|
if (Modbus is null)
|
||||||
|
{
|
||||||
|
var connection = new ModbusTcpConnection(Hostname, Port);
|
||||||
|
Modbus = new ModbusTcpClient(connection, SlaveAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AmptCommunicationUnitStatus TryReadStatus()
|
||||||
|
{
|
||||||
|
var r = Modbus!.ReadHoldingRegisters(1, 116);
|
||||||
|
|
||||||
|
var currentFactor = Pow(10.0m, r.GetInt16(73));
|
||||||
|
var voltageFactor = Pow(10.0m, r.GetInt16(74));
|
||||||
|
var energyFactor = Pow(10.0m, r.GetInt16(76) + 3); // +3 => converted from Wh to kWh
|
||||||
var nbrOfDevices = r.GetUInt16(78);
|
var nbrOfDevices = r.GetUInt16(78);
|
||||||
|
|
||||||
var devices = Enumerable
|
var devices = Enumerable
|
||||||
|
@ -48,84 +74,45 @@ public class AmptCommunicationUnit
|
||||||
.Select(ReadDeviceStatus)
|
.Select(ReadDeviceStatus)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var amptSt = new AmptStatus
|
return new AmptCommunicationUnitStatus
|
||||||
(
|
|
||||||
Sid : r.GetUInt32(1),
|
|
||||||
IdSunSpec : r.GetUInt16(3),
|
|
||||||
Manufacturer : r.GetString(5, 16),
|
|
||||||
Model : r.GetString(21, 16),
|
|
||||||
Version : r.GetString(45, 8),
|
|
||||||
SerialNumber : r.GetString(53, 16),
|
|
||||||
DeviceAddress : r.GetInt16(69),
|
|
||||||
IdVendor : r.GetUInt16(71),
|
|
||||||
Devices : devices
|
|
||||||
// devices.d Current1 = r.GetInt16(90) * currentFactor,
|
|
||||||
// Current2 = r.GetInt16(106) * currentFactor,
|
|
||||||
// Voltage1 = r.GetUInt32(91) * voltageFactor,
|
|
||||||
// Voltage2 = r.GetUInt32(107) * voltageFactor
|
|
||||||
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return amptSt;
|
|
||||||
|
|
||||||
Decimal ReadDevicesVoltage(Int32 numberOfDevice)
|
|
||||||
{
|
{
|
||||||
var avgVoltage = 0.0m;
|
Sid = r.GetUInt32(1),
|
||||||
|
IdSunSpec = r.GetUInt16(3),
|
||||||
for (var i = 0; i < numberOfDevice; i++)
|
Manufacturer = r.GetString(5, 16),
|
||||||
|
Model = r.GetString(21, 16),
|
||||||
|
Version = r.GetString(45, 8),
|
||||||
|
SerialNumber = r.GetString(53, 16),
|
||||||
|
DeviceAddress = r.GetInt16(69),
|
||||||
|
IdVendor = r.GetUInt16(71),
|
||||||
|
Devices = devices
|
||||||
|
};
|
||||||
|
|
||||||
|
AmptStatus ReadDeviceStatus(Int32 deviceNumber)
|
||||||
|
{
|
||||||
|
var baseAddress = (UInt16)(FirstDeviceOffset + deviceNumber * RegistersPerDevice); // base address
|
||||||
|
|
||||||
|
return new AmptStatus
|
||||||
{
|
{
|
||||||
var b = (UInt16)(FirstDeviceOffset + i * RegistersPerDevice); // base address
|
Dc = new DcBus
|
||||||
|
|
||||||
avgVoltage+= r.GetUInt32((UInt16)(b + 6)) * voltageFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
return avgVoltage / numberOfDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
Decimal ReadDevicesCurrent(Int32 numberOfDevice)
|
|
||||||
{
|
|
||||||
Decimal avgCurrent = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < numberOfDevice; i++)
|
|
||||||
{
|
|
||||||
var b = (UInt16)(FirstDeviceOffset + i * RegistersPerDevice); // base address
|
|
||||||
|
|
||||||
avgCurrent+= r!.GetUInt32((UInt16)(b + 5)) * voltageFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
return avgCurrent / numberOfDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
AmptDeviceStatus ReadDeviceStatus(Int32 deviceNumber)
|
|
||||||
{
|
|
||||||
var b = (UInt16)(FirstDeviceOffset + deviceNumber * RegistersPerDevice); // base address
|
|
||||||
|
|
||||||
return new AmptDeviceStatus
|
|
||||||
(
|
|
||||||
Dc : new DcConnection
|
|
||||||
(
|
|
||||||
Voltage:r.GetUInt32((UInt16)(b + 6)) * voltageFactor,
|
|
||||||
Current:r.GetUInt16((UInt16)(b + 5)) * currentFactor
|
|
||||||
),
|
|
||||||
DeviceId : r.GetInt16 (b) ,
|
|
||||||
Timestamp : r.GetUInt32((UInt16)(b + 3)),
|
|
||||||
ProductionToday : r.GetUInt32((UInt16)(b + 12))* energyFactor,
|
|
||||||
|
|
||||||
Strings : new []
|
|
||||||
{
|
{
|
||||||
new DcConnection
|
Voltage = r.GetUInt32((UInt16)(baseAddress + 6)) * voltageFactor,
|
||||||
(
|
Current = r.GetUInt16((UInt16)(baseAddress + 5)) * currentFactor
|
||||||
Voltage : r.GetUInt32((UInt16)(b + 8)) * voltageFactor,
|
},
|
||||||
Current : r.GetUInt16((UInt16)(b + 14)) * currentFactor
|
Strings = new DcBus[]
|
||||||
),
|
{
|
||||||
new DcConnection
|
new()
|
||||||
(
|
{
|
||||||
Voltage : r.GetUInt32((UInt16)(b + 9)) * voltageFactor,
|
Voltage = r.GetUInt32((UInt16)(baseAddress + 8)) * voltageFactor,
|
||||||
Current : r.GetUInt16((UInt16)(b + 15)) * currentFactor
|
Current = r.GetUInt16((UInt16)(baseAddress + 14)) * currentFactor
|
||||||
)
|
},
|
||||||
}
|
new()
|
||||||
);
|
{
|
||||||
|
Voltage = r.GetUInt32((UInt16)(baseAddress + 9)) * voltageFactor,
|
||||||
|
Current = r.GetUInt16((UInt16)(baseAddress + 15)) * currentFactor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ProductionToday = r.GetUInt32((UInt16)(baseAddress + 12)) * energyFactor,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
|
||||||
|
public record AmptCommunicationUnitStatus
|
||||||
|
{
|
||||||
|
public UInt32 Sid { get; init; } // A well-known value 0x53756e53, uniquely identifies this as a SunSpec Modbus Map
|
||||||
|
public UInt16 IdSunSpec { get; init; } // A well-known value 1, uniquely identifies this as a SunSpec Common Model
|
||||||
|
|
||||||
|
public String Manufacturer { get; init; } = "undefined"; // A well-known value registered with SunSpec for compliance: "Ampt"
|
||||||
|
public String Model { get; init; } = "undefined"; // Manufacturer specific value "Communication Unit"
|
||||||
|
public String Version { get; init; } = "undefined"; // Software Version
|
||||||
|
public String SerialNumber { get; init; } = "undefined"; // Manufacturer specific value
|
||||||
|
public Int16 DeviceAddress { get; init; } // Modbus Device ID
|
||||||
|
public UInt16 IdVendor { get; init; } // Ampt SunSpec Vendor Code 64050
|
||||||
|
|
||||||
|
public IReadOnlyList<AmptStatus> Devices { get; init; } = Array.Empty<AmptStatus>();
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
using InnovEnergy.Lib.StatusApi;
|
|
||||||
using InnovEnergy.Lib.StatusApi.Connections;
|
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.AMPT;
|
|
||||||
|
|
||||||
public record AmptDeviceStatus
|
|
||||||
(
|
|
||||||
DcConnection Dc,
|
|
||||||
// UInt16 NbrOfStrings,
|
|
||||||
Int16 DeviceId, // The string number
|
|
||||||
UInt32 Timestamp, // The UTC timestamp of the measurements
|
|
||||||
Decimal ProductionToday, // converted to kW in AmptCU class
|
|
||||||
IReadOnlyList<DcConnection> Strings
|
|
||||||
): MpptStatus(Dc, Strings)
|
|
||||||
{}
|
|
|
@ -1,24 +1,9 @@
|
||||||
|
using InnovEnergy.Lib.StatusApi;
|
||||||
|
using InnovEnergy.Lib.Units;
|
||||||
|
|
||||||
namespace InnovEnergy.Lib.Devices.AMPT;
|
namespace InnovEnergy.Lib.Devices.AMPT;
|
||||||
|
|
||||||
public record AmptStatus
|
public record AmptStatus : MpptStatus
|
||||||
(
|
|
||||||
UInt32 Sid, // A well-known value 0x53756e53, uniquely identifies this as a SunSpec Modbus Map
|
|
||||||
UInt16 IdSunSpec, // A well-known value 1, uniquely identifies this as a SunSpec Common Model
|
|
||||||
// UInt16 L, // Well-known # of 16-bit registers to follow : 66
|
|
||||||
String? Manufacturer, // A well-known value registered with SunSpec for compliance: "Ampt"
|
|
||||||
String? Model, // Manufacturer specific value "Communication Unit"
|
|
||||||
String? Version, // Software Version
|
|
||||||
String? SerialNumber, // Manufacturer specific value
|
|
||||||
Int16 DeviceAddress, // Modbus Device ID
|
|
||||||
UInt16 IdVendor, // Ampt SunSpec Vendor Code 64050
|
|
||||||
// Decimal Current1,
|
|
||||||
// Decimal Current2,
|
|
||||||
// Decimal Voltage1,
|
|
||||||
// Decimal Voltage2,
|
|
||||||
IReadOnlyList<AmptDeviceStatus> Devices
|
|
||||||
//internal const UInt16 StartRegister = 1;
|
|
||||||
//internal const UInt16 TotalNbOfRegister = 116;
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
|
public Energy ProductionToday { get; init; } // converted to kW in AmptCU class
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
namespace InnovEnergy.Lib.Devices.AMPT;
|
|
||||||
|
|
||||||
public record AmptStringStatus
|
|
||||||
{
|
|
||||||
public Decimal Voltage { get; init; }
|
|
||||||
public Decimal Current { get; init; }
|
|
||||||
}
|
|
|
@ -7,31 +7,60 @@ namespace InnovEnergy.Lib.Devices.Adam6060;
|
||||||
|
|
||||||
public class Adam6060Device
|
public class Adam6060Device
|
||||||
{
|
{
|
||||||
private ModbusTcpClient Modbus { get; }
|
public String Hostname { get; }
|
||||||
|
public UInt16 Port { get; }
|
||||||
|
public Byte SlaveAddress { get; }
|
||||||
|
|
||||||
|
private ModbusTcpClient? Modbus { get; set; }
|
||||||
|
|
||||||
public Adam6060Device(String hostname, UInt16 port = 5004, Byte slaveAddress = 2)
|
public Adam6060Device(String hostname, UInt16 port = 5004, Byte slaveAddress = 2)
|
||||||
{
|
{
|
||||||
var connection = new ModbusTcpConnection(hostname, port);
|
Hostname = hostname;
|
||||||
Modbus = new ModbusTcpClient(connection, slaveAddress);
|
Port = port;
|
||||||
|
SlaveAddress = slaveAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenConnection()
|
||||||
|
{
|
||||||
|
if (Modbus is null)
|
||||||
|
{
|
||||||
|
var connection = new ModbusTcpConnection(Hostname, Port);
|
||||||
|
Modbus = new ModbusTcpClient(connection, SlaveAddress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Adam6060Status? ReadStatus()
|
public Adam6060Status? ReadStatus()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
OpenConnection();
|
||||||
return TryReadStatus();
|
return TryReadStatus();
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch
|
||||||
{
|
{
|
||||||
Modbus.CloseConnection();
|
CloseConnection();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CloseConnection()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Modbus?.CloseConnection();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
Modbus = null;
|
||||||
|
}
|
||||||
|
|
||||||
private Adam6060Status TryReadStatus()
|
private Adam6060Status TryReadStatus()
|
||||||
{
|
{
|
||||||
var inputs = Modbus.ReadDiscreteInputs(DigitalInputsStartRegister, NbDigitalInputs);
|
var inputs = Modbus!.ReadDiscreteInputs(DigitalInputsStartRegister, NbDigitalInputs);
|
||||||
var relays = Modbus.ReadDiscreteInputs(RelaysStartRegister, NbRelays);
|
var relays = Modbus!.ReadDiscreteInputs(RelaysStartRegister, NbRelays);
|
||||||
|
|
||||||
return new Adam6060Status
|
return new Adam6060Status
|
||||||
{
|
{
|
||||||
|
@ -55,17 +84,19 @@ public class Adam6060Device
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Modbus.WriteMultipleCoils(RelaysStartRegister, control.Relay0,
|
OpenConnection();
|
||||||
control.Relay1,
|
|
||||||
control.Relay2,
|
Modbus!.WriteMultipleCoils(RelaysStartRegister, control.Relay0,
|
||||||
control.Relay3,
|
control.Relay1,
|
||||||
control.Relay4,
|
control.Relay2,
|
||||||
control.Relay5);
|
control.Relay3,
|
||||||
|
control.Relay4,
|
||||||
|
control.Relay5);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch
|
||||||
{
|
{
|
||||||
Modbus.CloseConnection();
|
CloseConnection();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,4 +34,11 @@ public readonly struct State : IReadOnlyList<String>
|
||||||
|
|
||||||
public Int32 Count => Values.Count;
|
public Int32 Count => Values.Count;
|
||||||
public String this[Int32 index] => Values[index];
|
public String this[Int32 index] => Values[index];
|
||||||
}
|
|
||||||
|
public static State<T> From<T>(T t) where T : Enum
|
||||||
|
{
|
||||||
|
return new State<T>(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace InnovEnergy.Lib.Units;
|
||||||
|
|
||||||
|
public readonly struct State<T> : IReadOnlyList<T> where T:Enum
|
||||||
|
{
|
||||||
|
public IReadOnlyList<T> Values { get; }
|
||||||
|
|
||||||
|
public State(IReadOnlyList<T> values) => Values = values;
|
||||||
|
|
||||||
|
public State(params T[] values) : this((IReadOnlyList<T>)values){}
|
||||||
|
public State(params State<T>[] states) : this((IReadOnlyList<T>)states.SelectMany(s => s.Values).ToList()){}
|
||||||
|
|
||||||
|
public static implicit operator State<T>(T s) => new((IReadOnlyList<T>)s);
|
||||||
|
public static implicit operator State<T>(List<T> s) => new((IReadOnlyList<T>)s);
|
||||||
|
public static implicit operator State<T>(T[] s) => new((IReadOnlyList<T>)s);
|
||||||
|
|
||||||
|
public static State<T> operator |(State<T> left, State<T> right) => new State<T>(left, right);
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator() => Values.GetEnumerator();
|
||||||
|
|
||||||
|
public override String ToString() => String.Join("; ", Values);
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
public Int32 Count => Values.Count;
|
||||||
|
public T this[Int32 index] => Values[index];
|
||||||
|
}
|
Loading…
Reference in New Issue