66 Commits

Author SHA1 Message Date
ff6919e54f Change AutoSave delay 2019-05-10 21:18:55 +02:00
9e8d1ee61b Fix AutoSave - Now Working ! 2019-05-10 21:17:13 +02:00
46cb938ea2 Fix client crash when historic is too big
Because you deserve it
2019-05-10 20:27:37 +02:00
b144629939 Add missing translations 2019-05-08 22:05:39 +02:00
0cf9b3a566 Fix app and save on id add 2019-04-29 22:35:59 +02:00
1072853fa5 Modify protocol according to doc 2019-04-29 22:21:45 +02:00
e47f9ef5b7 Try to fix client 2019-04-29 22:18:18 +02:00
d932207db0 Fix broker historic manager 2019-04-29 21:04:26 +02:00
b225953dbe Fix client add button 2019-04-29 19:52:48 +02:00
929b49a05c Change broker response on invalid id
Now just send back a blank response
2019-04-29 19:45:52 +02:00
6ca735b574 Add client add button 2019-04-29 19:35:38 +02:00
216daeac33 Fix broker disposing client 2019-04-29 19:10:39 +02:00
7467d5ba81 Implement IDisposable for CommandStream 2019-04-27 13:59:01 +02:00
76bbefd729 Fix Broker Autosave 2019-04-27 13:58:45 +02:00
29329ee303 Add app refresh 2019-04-27 13:49:02 +02:00
c6888550b6 Add app add button 2019-04-27 13:29:53 +02:00
d8c7a9e2c5 Add commands for Broker, autosave and clean logging 2019-04-27 11:29:31 +02:00
f67d070fc3 Fix some things 2019-04-27 11:01:38 +02:00
86593eff22 Fix typo 2019-04-24 23:45:42 +02:00
a5e75e7e14 Add Client icon per type
Only added Ficus Icon
2019-04-24 23:39:36 +02:00
368f0b99df Update Protocol, parsing errors 2019-04-24 23:14:57 +02:00
b4bf7aad20 Client now connects to the broker 2019-04-24 23:14:35 +02:00
ab85e78f4c Update Broker 2019-04-24 23:14:10 +02:00
0f91751087 Add Broker Info command client-side 2019-04-24 20:07:34 +02:00
e3cc6392a7 End Broker 2019-04-24 19:27:40 +02:00
a1e1d43a6f ˅ 2019-04-22 17:28:56 +02:00
bf8ddcb738 Fix value format on charts 2019-04-22 17:27:34 +02:00
9e78307541 Update commands accorded to doc
Update Captors and Info commands
2019-04-22 16:54:16 +02:00
25891c6c9d Add HistoricPage 2019-04-22 14:12:49 +02:00
54ecf641f5 Change colors of CaptorsPage 2019-04-22 14:12:36 +02:00
0396b66606 Fix language selection page
Friendly name display
2019-04-18 15:02:23 +02:00
f9adda14b8 Adjust icons opacity
Full black isn't catchy
2019-04-17 13:48:34 +02:00
198a6ddebc Begin CaptorsPage 2019-04-17 13:44:24 +02:00
bc4615761c Add StatusBar control 2019-04-11 15:11:21 +02:00
684393f85d Add PlantPage logic 2019-04-07 15:50:00 +02:00
5dc9007ad2 Add title to PlantPage
Set Plant Name as Title
2019-03-27 19:18:46 +01:00
8be57fb3df Change home item colors 2019-03-27 19:16:42 +01:00
c86e649b4a Add back to show menu on home 2019-03-26 18:06:53 +01:00
1fbcaacd63 See previous 2019-03-25 23:00:41 +01:00
9cc08ff10b Fix language setting null default value 2019-03-25 22:58:56 +01:00
c96e8fc6ce Refactor Menu 2019-03-25 22:01:27 +01:00
e64979e42f End HomePage interface
Logic is coming... soon or not
2019-03-25 21:48:48 +01:00
35e468e0b0 Add HomePage 2019-03-24 19:40:53 +01:00
46f914fe4d Separate Client and Server port 2019-03-24 12:50:03 +01:00
fa8608b44b Change CaptorsResponse according to Wiki 2019-03-24 12:48:33 +01:00
73855e246c Refactor using extensions 2019-03-24 12:14:28 +01:00
ed635c5ec2 See previous 2019-03-24 11:34:49 +01:00
6d7a9969cd Add Info Command 2019-03-24 11:34:35 +01:00
b02114f0f9 Fix coding style in PingCommand
new string[] -> new[]
2019-03-23 22:41:13 +01:00
f03de89815 Add HistoricResponse 2019-03-23 22:34:45 +01:00
a1944a1203 Add default Command to each 2019-03-23 22:19:42 +01:00
aa20f9d466 Add default Command to CommandSerializable<T> 2019-03-23 22:17:14 +01:00
18b9a97b91 Add CaptorsRequest 2019-03-23 22:12:09 +01:00
ea67a8069f Change CaptorsRequest to HistoricRequest
Should I sleep?
2019-03-23 22:12:00 +01:00
d81bf5d8ec Add CaptorsRequest 2019-03-23 22:07:09 +01:00
d7d60f30af Move CaptorsCommand to CaptorsResponse 2019-03-23 21:58:06 +01:00
59c146c45f Add CaptorsCommand 2019-03-23 21:55:42 +01:00
1c49b5150a Rename Ping to PingCommand 2019-03-23 21:47:26 +01:00
6b7ee73597 Add CommandStream.Receive<T> 2019-03-23 14:10:51 +01:00
4eff8e32d8 Add Ping command 2019-03-23 13:34:56 +01:00
233523ca4d Add doc 2019-03-23 11:37:52 +01:00
faa54aca0e Add CommandSerializable
Used to easily serialize class to CommandPacket
2019-03-22 19:57:33 +01:00
58fbaf4e35 Add Protocol implementation 2019-03-19 21:02:10 +01:00
268e04a710 Change version
From 1.0.0.0 to 0.1.0.0
2019-03-16 22:17:01 +01:00
7e7109d609 Change Settings and About pages
Added Media tab in About
Language and notifications settings
2019-03-16 22:11:47 +01:00
9af2bf392d Clean HomePage 2019-03-11 19:44:07 +01:00
93 changed files with 6342 additions and 524 deletions

93
PlantBox.Broker/Broker.cs Normal file
View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PlantBox.Broker
{
class Broker
{
public PlantBoxesManager PlantBoxesManager { get; }
public ClientManager ClientManager { get; }
public ServerManager ServerManager { get; }
public bool IsRunning { get; set; } = true;
private Timer _autoSaveTimer;
public Broker(string[] args)
{
Console.WriteLine("Initializing Broker...");
PlantBoxesManager = new PlantBoxesManager();
PlantBoxesManager.Load();
PlantBoxesManager.UpdatePlantsState();
ClientManager = new ClientManager(this);
ServerManager = new ServerManager(this);
}
public void Start()
{
Task.Run(() => ClientManager.Start());
Task.Run(() => ServerManager.Start());
// Auto-Save
_autoSaveTimer = new Timer(OnSave, null, TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(15));
string input;
do
{
input = Console.ReadLine().ToLowerInvariant();
string[] split = input.Split(" ");
input = split[0];
string[] args = split.Skip(1).ToArray();
ExeCommand(input.ToLower(), args);
} while (input != "exit" && input != "stop" && input != "quit");
Console.WriteLine("Stopping Broker...");
IsRunning = false;
_autoSaveTimer.Dispose();
PlantBoxesManager.Save();
}
private void OnSave(object state)
{
PlantBoxesManager.Save();
}
private void ExeCommand(string command, string[] arguments)
{
switch (command)
{
case "save":
PlantBoxesManager.Save();
break;
case "clear":
Console.WriteLine($"Clearing {arguments[0]}...");
if (ulong.TryParse(arguments[0], out ulong id) && PlantBoxesManager[id] != null)
{
PlantBoxesManager.ClearPlantBox(PlantBoxesManager[id]);
Console.WriteLine($"{id} cleared");
}
else
{
Console.WriteLine("Invalid id");
}
break;
case "list":
foreach (var plant in PlantBoxesManager)
{
Console.WriteLine(plant);
}
break;
}
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Broker
{
class CaptorsValue
{
public double Humidity { get; set; }
public double Luminosity { get; set; }
public double Temperature { get; set; }
public CaptorsValue()
{
}
public CaptorsValue(double humidity, double luminosity, double temperature)
{
Humidity = humidity;
Luminosity = luminosity;
Temperature = temperature;
}
public CaptorsValue((double humidity, double luminosity, double temperature) tuple) : this(tuple.humidity, tuple.luminosity, tuple.temperature)
{
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PlantBox.Shared.Communication;
using PlantBox.Shared.Communication.Commands;
namespace PlantBox.Broker
{
class ClientManager : TcpManager
{
public ClientManager(Broker broker) : base(broker)
{
}
protected override string LogPrefix => "Client";
protected override int ListeningPort => Connection.TCP_CLIENT_PORT;
protected override void CaptorsCommand(CommandStream commandStream, CommandPacket packet)
{
CaptorsRequest captorsRequest = new CaptorsRequest().Deserialize(packet.Arguments);
ulong id = packet.ID;
PlantBox plantBox = Broker.PlantBoxesManager[id];
CaptorsResponse response;
if (plantBox == null)
{
response = new CaptorsResponse(0, 0, 0, 0);
}
else
{
response = new CaptorsResponse(plantBox.Humidity.Value, plantBox.Luminosity.Value, plantBox.Temperature.Value, plantBox.TankLevel);
}
commandStream.Send(response.ToCommandPacket(id));
}
protected override void HistoricCommand(CommandStream commandStream, CommandPacket packet)
{
double[] FillArray(double[] values, int length)
{
// Check if need
if (values.Length >= length)
{
return values.Reverse().Take(length).Reverse().ToArray();
}
var array = new double[length];
if (values.Length > 0)
{
int startIndex = length - values.Length;
values.CopyTo(array, startIndex);
}
return array;
}
HistoricRequest historicRequest = new HistoricRequest().Deserialize(packet.Arguments);
ulong id = packet.ID;
PlantBox plantBox = Broker.PlantBoxesManager[id];
int count = historicRequest.Number;
HistoricResponse response;
if (plantBox == null)
{
response = new HistoricResponse(TimeSpan.MinValue, Array.Empty<double>(), Array.Empty<double>(), Array.Empty<double>());
}
else
{
HistoricManager historic = plantBox.HistoricManager;
IReadOnlyList<CaptorsValue> captorsValues;
switch (historicRequest.Interval)
{
case HistoricInterval.Minutely:
captorsValues = historic.MinutesHistoric;
break;
case HistoricInterval.Hourly:
captorsValues = historic.HoursHistoric;
break;
case HistoricInterval.Daily:
captorsValues = historic.DaysHistoric;
break;
case HistoricInterval.Weekly:
captorsValues = historic.WeeksHistoric;
break;
case HistoricInterval.Monthly:
captorsValues = historic.MonthsHistoric;
break;
default:
throw new InvalidOperationException("How did you just got here? Even 『 』can't");
}
response = new HistoricResponse
(
DateTime.Now - plantBox.LastMeasureDate,
FillArray(captorsValues.Select(x => x.Humidity).ToArray(), count),
FillArray(captorsValues.Select(x => x.Luminosity).ToArray(), count),
FillArray(captorsValues.Select(x => x.Temperature).ToArray(), count)
);
}
commandStream.Send(response.ToCommandPacket(id));
}
protected override void InfoCommand(CommandStream commandStream, CommandPacket packet)
{
InfoRequest infoRequest = new InfoRequest().Deserialize(packet.Arguments);
ulong id = packet.ID;
PlantBox plantBox = Broker.PlantBoxesManager[id];
InfoResponse response;
if (plantBox == null)
{
response = new InfoResponse("", default, default, 0, 0, 0, 0, 0, 0);
}
else
{
plantBox.UpdateState();
response = new InfoResponse
(
plantBox.Name, plantBox.Type, plantBox.State,
plantBox.Humidity.Min, plantBox.Humidity.Max,
plantBox.Luminosity.Min, plantBox.Luminosity.Max,
plantBox.Temperature.Min, plantBox.Temperature.Max
);
}
commandStream.Send(response.ToCommandPacket(id));
}
protected override void PingCommand(CommandStream commandStream, CommandPacket packet)
{
var ping = new PingCommand().Deserialize(packet.Arguments);
commandStream.Send(ping.ToCommandPacket(packet.ID));
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Broker
{
public static class Extensions
{
public static IEnumerable<T> TakeFromEnd<T>(this List<T> list, int count)
{
return list.GetRange(list.Count - count, count);
}
}
}

View File

@@ -0,0 +1,97 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PlantBox.Broker
{
class HistoricManager
{
[JsonProperty(PropertyName = nameof(MinutesHistoric))]
private List<CaptorsValue> _minutesHistoric;
[JsonIgnore]
public IReadOnlyList<CaptorsValue> MinutesHistoric => _minutesHistoric.AsReadOnly();
[JsonProperty(PropertyName = nameof(HoursHistoric))]
private List<CaptorsValue> _hoursHistoric;
[JsonIgnore]
public IReadOnlyList<CaptorsValue> HoursHistoric => _hoursHistoric.AsReadOnly();
[JsonProperty(PropertyName = nameof(DaysHistoric))]
private List<CaptorsValue> _daysHistoric;
[JsonIgnore]
public IReadOnlyList<CaptorsValue> DaysHistoric => _daysHistoric.AsReadOnly();
[JsonProperty(PropertyName = nameof(WeeksHistoric))]
private List<CaptorsValue> _weeksHistoric;
[JsonIgnore]
public IReadOnlyList<CaptorsValue> WeeksHistoric => _weeksHistoric.AsReadOnly();
[JsonProperty(PropertyName = nameof(MonthsHistoric))]
private List<CaptorsValue> _monthsHistoric;
[JsonIgnore]
public IReadOnlyList<CaptorsValue> MonthsHistoric => _monthsHistoric.AsReadOnly();
public HistoricManager()
{
_minutesHistoric = new List<CaptorsValue>();
_hoursHistoric = new List<CaptorsValue>();
_daysHistoric = new List<CaptorsValue>();
_weeksHistoric = new List<CaptorsValue>();
_monthsHistoric = new List<CaptorsValue>();
}
public HistoricManager(List<CaptorsValue> minutesHistoric, List<CaptorsValue> hoursHistoric, List<CaptorsValue> daysHistoric, List<CaptorsValue> weeksHistoric, List<CaptorsValue> monthsHistoric)
{
_minutesHistoric = minutesHistoric;
_hoursHistoric = hoursHistoric;
_daysHistoric = daysHistoric;
_weeksHistoric = weeksHistoric;
_monthsHistoric = monthsHistoric;
}
public void Add(CaptorsValue captorsValue)
{
_minutesHistoric.Add(captorsValue);
if (_minutesHistoric.Count % 12 == 0)
{
_hoursHistoric.Add(new CaptorsValue(GetAverage(_minutesHistoric.TakeFromEnd(12))));
if (_hoursHistoric.Count % 24 == 0)
{
_daysHistoric.Add(new CaptorsValue(GetAverage(_hoursHistoric.TakeFromEnd(24))));
if (_daysHistoric.Count % 7 == 0)
{
_weeksHistoric.Add(new CaptorsValue(GetAverage(_daysHistoric.TakeFromEnd(7))));
if (_daysHistoric.Count % 31 == 0)
{
_monthsHistoric.Add(new CaptorsValue(GetAverage(_daysHistoric.TakeFromEnd(31))));
}
}
}
}
}
public void Clear()
{
_minutesHistoric.Clear();
_hoursHistoric.Clear();
_daysHistoric.Clear();
_weeksHistoric.Clear();
_monthsHistoric.Clear();
}
private (double humidity, double luminosity, double temperature) GetAverage(IEnumerable<CaptorsValue> captorsValues)
{
return
(
captorsValues.Average(x => x.Humidity),
captorsValues.Average(x => x.Luminosity),
captorsValues.Average(x => x.Temperature)
);
}
}
}

View File

@@ -7,6 +7,10 @@
<Description>Broker of the PlantBox project</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PlantBox.Shared\PlantBox.Shared.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,63 @@
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Broker
{
class PlantBox
{
// General Info
public ulong ID { get; set; }
public string Name { get; set; }
public PlantType Type { get; set; }
public PlantState State { get; set; }
// Captors
public DateTime LastMeasureDate { get; set; }
public double TankLevel { get; set; }
public CaptorValue Humidity { get; set; }
public CaptorValue Luminosity { get; set; }
public CaptorValue Temperature { get; set; }
// Historic
public HistoricManager HistoricManager { get; set; }
// State conditions
public void UpdateState()
{
bool IsDay()
{
double hour = DateTime.Now.Hour;
return hour > 8 && hour < 20;
}
if (Humidity.Value < Humidity.Min || Humidity.Value > Humidity.Max)
{
State = PlantState.Warning;
}
if (IsDay() && Luminosity.Value < Luminosity.Min || Luminosity.Value > Luminosity.Max)
{
State = PlantState.Warning;
}
if (Temperature.Value < Temperature.Min || Temperature.Value > Temperature.Max)
{
State = PlantState.Warning;
}
if (TankLevel < 20)
{
State = PlantState.Warning;
}
if ((DateTime.Now - LastMeasureDate).TotalMinutes >= 7)
{
State = PlantState.Bad;
}
}
public override string ToString()
{
return $"{ID}:\n Name: {Name}\n Type: {Type}\n State: {State}";
}
}
}

View File

@@ -0,0 +1,87 @@
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace PlantBox.Broker
{
class PlantBoxesManager : IEnumerable<PlantBox>
{
public string FilePath => Path.Combine(Environment.CurrentDirectory, FileName);
public string FileName => "storage.json";
private Dictionary<ulong, PlantBox> _plantBoxes;
public PlantBoxesManager()
{
}
public PlantBox this[ulong id]
{
get => _plantBoxes.GetValueOrDefault(id);
}
public PlantBox GetPlantBox(ulong id) => this[id];
public void Add(PlantBox plantBox)
{
ulong id = plantBox.ID;
if (_plantBoxes.ContainsKey(id))
{
_plantBoxes[id] = plantBox;
}
else
{
_plantBoxes.Add(id, plantBox);
}
}
public void UpdatePlantsState()
{
foreach (PlantBox plantBox in _plantBoxes.Values)
{
plantBox.UpdateState();
}
}
public void Load()
{
Console.WriteLine("Loading storage...");
if (File.Exists(FilePath))
{
_plantBoxes = JsonConvert.DeserializeObject<Dictionary<ulong, PlantBox>>(File.ReadAllText(FilePath));
}
else
{
_plantBoxes = new Dictionary<ulong, PlantBox>();
}
Console.WriteLine("Storage loaded");
}
public void Save()
{
Console.WriteLine("Saving storage...");
File.WriteAllText(FilePath, JsonConvert.SerializeObject(_plantBoxes));
Console.WriteLine("Storage saved");
}
public void ClearPlantBox(PlantBox plantBox)
{
if (plantBox != null)
{
plantBox.HistoricManager.Clear();
}
}
public IEnumerator<PlantBox> GetEnumerator() => _plantBoxes.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@@ -1,12 +1,21 @@
using System;
using PlantBox.Shared.Communication;
using PlantBox.Shared.Communication.Commands;
using System;
using System.Net;
using System.Net.Sockets;
namespace PlantBox.Broker
{
class Program
{
public static Broker Broker { get; private set; }
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Broker = new Broker(args);
Broker.Start();
Console.WriteLine("Broker stopped");
}
}
}

View File

@@ -0,0 +1,83 @@
using PlantBox.Shared.Communication;
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace PlantBox.Broker
{
class ServerManager : TcpManager
{
public ServerManager(Broker broker) : base(broker)
{
}
protected override string LogPrefix => "Server";
protected override int ListeningPort => Connection.TCP_SERVER_PORT;
protected override void InfoCommand(CommandStream commandStream, CommandPacket packet)
{
InfoResponse infoResponse = new InfoResponse().Deserialize(packet.Arguments);
ulong id = packet.ID;
PlantBox plantBox = Broker.PlantBoxesManager[id];
if (plantBox == null)
{
plantBox = new PlantBox
{
HistoricManager = new HistoricManager()
};
}
plantBox.ID = id;
plantBox.Name = infoResponse.Name;
plantBox.Type = infoResponse.Type;
plantBox.State = infoResponse.State;
plantBox.Humidity = new CaptorValue(infoResponse.HumidityMin, infoResponse.HumidityMax, plantBox.Humidity?.Value ?? 0);
plantBox.Luminosity = new CaptorValue(infoResponse.LuminosityMin, infoResponse.LuminosityMax, plantBox.Luminosity?.Value ?? 0);
plantBox.Temperature = new CaptorValue(infoResponse.TemperatureMin, infoResponse.TemperatureMax, plantBox.Temperature?.Value ?? 0);
plantBox.UpdateState();
Broker.PlantBoxesManager.Add(plantBox);
}
protected override void CaptorsCommand(CommandStream commandStream, CommandPacket packet)
{
CaptorsResponse captorsResponse = new CaptorsResponse().Deserialize(packet.Arguments);
ulong id = packet.ID;
PlantBox plantBox = Broker.PlantBoxesManager[id];
if (plantBox == null)
{
Log($"Received captors info from non-registered PlantBox ({id}), ignoring it");
return;
}
plantBox.LastMeasureDate = DateTime.Now;
plantBox.Humidity.Value = captorsResponse.Humidity;
plantBox.Luminosity.Value = captorsResponse.Luminosity;
plantBox.Temperature.Value = captorsResponse.Temperature;
plantBox.TankLevel = captorsResponse.Tank;
plantBox.UpdateState();
plantBox.HistoricManager.Add(new CaptorsValue(plantBox.Humidity.Value, plantBox.Luminosity.Value, plantBox.Temperature.Value));
}
protected override void HistoricCommand(CommandStream commandStream, CommandPacket packet)
{
throw new NotImplementedException();
}
protected override void PingCommand(CommandStream commandStream, CommandPacket packet)
{
var ping = new PingCommand().Deserialize(packet.Arguments);
commandStream.Send(ping.ToCommandPacket(packet.ID));
}
}
}

View File

@@ -0,0 +1,101 @@
using PlantBox.Shared.Communication;
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace PlantBox.Broker
{
abstract class TcpManager
{
public Broker Broker { get; }
protected abstract string LogPrefix { get; }
protected abstract int ListeningPort { get; }
private TcpListener _listener;
public TcpManager(Broker broker)
{
Log("Initializing...");
Broker = broker;
_listener = new TcpListener(IPAddress.Any, ListeningPort);
}
public void Start()
{
Log("Starting...");
_listener.Start();
Log("Started");
TcpLoop();
}
private void TcpLoop()
{
while (Broker.IsRunning)
{
Log("Waiting client...");
var client = _listener.AcceptTcpClient();
Log($"Found client: {client.Client.RemoteEndPoint}");
ClientLoop(client);
}
}
private void ClientLoop(TcpClient client)
{
try
{
using (var commandStream = new CommandStream(client.GetStream()))
{
while (client.Connected)
{
var packet = commandStream.Receive();
Log($"Received command from {client.Client.RemoteEndPoint}");
Log(packet.ToString());
switch (packet.Command)
{
case Command.Captors:
CaptorsCommand(commandStream, packet);
break;
case Command.Historic:
HistoricCommand(commandStream, packet);
break;
case Command.Info:
InfoCommand(commandStream, packet);
break;
case Command.Ping:
PingCommand(commandStream, packet);
break;
}
}
}
client.Close();
}
catch (Exception ex)
{
Log($"Client disconnected: {ex.Message}");
}
}
protected void Log(string message)
{
Console.WriteLine($"[{LogPrefix}]({DateTime.Now:HH:mm:ss}) {message}");
}
protected abstract void CaptorsCommand(CommandStream commandStream, CommandPacket packet);
protected abstract void HistoricCommand(CommandStream commandStream, CommandPacket packet);
protected abstract void InfoCommand(CommandStream commandStream, CommandPacket packet);
protected abstract void PingCommand(CommandStream commandStream, CommandPacket packet);
}
}

View File

@@ -33,7 +33,9 @@
<AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM>
<BundleAssemblies>false</BundleAssemblies>
<EmbedAssembliesIntoApk>false</EmbedAssembliesIntoApk>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<MandroidI18n />
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@@ -44,6 +46,10 @@
<WarningLevel>4</WarningLevel>
<AndroidManagedSymbols>true</AndroidManagedSymbols>
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
<AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM>
<BundleAssemblies>false</BundleAssemblies>
<MandroidI18n />
</PropertyGroup>
<ItemGroup>
<Reference Include="Mono.Android" />
@@ -53,12 +59,12 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Forms" Version="3.4.0.1008975" />
<PackageReference Include="Xamarin.Android.Support.Design" Version="27.0.2.1" />
<PackageReference Include="Xamarin.Android.Support.v7.AppCompat" Version="27.0.2.1" />
<PackageReference Include="Xamarin.Android.Support.v4" Version="27.0.2.1" />
<PackageReference Include="Xamarin.Android.Support.v7.CardView" Version="27.0.2.1" />
<PackageReference Include="Xamarin.Android.Support.v7.MediaRouter" Version="27.0.2.1" />
<PackageReference Include="Xamarin.Forms" Version="3.6.0.344457" />
<PackageReference Include="Xamarin.Android.Support.Design" Version="28.0.0.1" />
<PackageReference Include="Xamarin.Android.Support.v7.AppCompat" Version="28.0.0.1" />
<PackageReference Include="Xamarin.Android.Support.v4" Version="28.0.0.1" />
<PackageReference Include="Xamarin.Android.Support.v7.CardView" Version="28.0.0.1" />
<PackageReference Include="Xamarin.Android.Support.v7.MediaRouter" Version="28.0.0.1" />
</ItemGroup>
<ItemGroup>
<Compile Include="MainActivity.cs" />
@@ -93,7 +99,6 @@
<Folder Include="Resources\drawable-xhdpi\" />
<Folder Include="Resources\drawable-xxhdpi\" />
<Folder Include="Resources\drawable-xxxhdpi\" />
<Folder Include="Resources\drawable\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PlantBox.Client\PlantBox.Client.csproj">
@@ -126,5 +131,8 @@
<Generator>MSBuild:UpdateGeneratedFiles</Generator>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\Add.png" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>

View File

@@ -2,4 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="fr.ilyx.PlantBox.Client.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27" />
<application android:label="PlantBox" android:icon="@mipmap/ic_launcher"></application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

View File

@@ -104,6 +104,7 @@
</AppxManifest>
</ItemGroup>
<ItemGroup>
<Content Include="Add.png" />
<Content Include="Assets\Icon\LargeTile.scale-100.png" />
<Content Include="Assets\Icon\LargeTile.scale-125.png" />
<Content Include="Assets\Icon\LargeTile.scale-150.png" />
@@ -189,8 +190,8 @@
</Page>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Forms" Version="3.4.0.1008975" />
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.1.5" />
<PackageReference Include="Xamarin.Forms" Version="3.6.0.344457" />
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PlantBox.Client\PlantBox.Client.csproj">

View File

@@ -148,6 +148,9 @@
<Reference Include="Xamarin.iOS" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Acr.UserDialogs">
<Version>7.0.4</Version>
</PackageReference>
<PackageReference Include="Xamarin.Forms" Version="3.4.0.1008975" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />

View File

@@ -1,8 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:PlantBox.Client.ViewModels"
x:Class="PlantBox.Client.App">
<Application.Resources>
<viewmodels:SettingsViewModel x:Key="Settings" />
<Style TargetType="ListView" >
<Setter Property="Margin" Value="{OnPlatform UWP='10, 2'}" />
</Style>
<Style TargetType="TableView" >
<Setter Property="Margin" Value="{OnPlatform UWP='10, 0'}" />
</Style>
<Color x:Key="HumidityColor">#2196f3</Color>
<Color x:Key="HumidityColorSecondary">#90caf9</Color>
<Color x:Key="LuminosityColor">#ffc107</Color>
<Color x:Key="LuminosityColorSecondary">#ffe082</Color>
<Color x:Key="TemperatureColor">#FF5722</Color>
<Color x:Key="TemperatureColorSecondary">#ffab91</Color>
</Application.Resources>
</Application>

View File

@@ -1,4 +1,6 @@
using PlantBox.Client.Forms;
using PlantBox.Client.Resources;
using PlantBox.Client.ViewModels;
using System;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
@@ -8,11 +10,23 @@ namespace PlantBox.Client
{
public partial class App : Application
{
public static SettingsViewModel Settings { get; private set; }
public static MainPage MasterPage { get; private set; }
public App()
{
InitializeComponent();
MainPage = new MainPage();
// Load settings
Settings = (SettingsViewModel)Resources["Settings"];
if (Settings.Language != null)
{
Locale.Culture = Settings.Language;
}
MasterPage = new MainPage();
MainPage = MasterPage;
}
protected override void OnStart()
@@ -23,6 +37,7 @@ namespace PlantBox.Client
protected override void OnSleep()
{
// Handle when your app sleeps
Settings.Save();
}
protected override void OnResume()

View File

@@ -0,0 +1,62 @@
using PlantBox.Client.Models;
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Xamarin.Forms;
namespace PlantBox.Client.Converters
{
class CaptorValueToDoubleConverter : IValueConverter
{
const double HUMIDITY_UPPER_BOUND = 100.0;
const double HUMIDITY_LOWER_BOUND = 0.0;
const double LUMINOSITY_UPPER_BOUND = 1200.0;
const double LUMINOSITY_LOWER_BOUND = 0.0;
const double TEMPERATURE_UPPER_BOUND = 50.0;
const double TEMPERATURE_LOWER_BOUND = -20.0;
const double TANK_UPPER_BOUND = 100.0;
const double TANK_LOWER_BOUND = 0.0;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double d && parameter is CaptorType type)
{
double upperBound;
double lowerBound;
switch (type)
{
case CaptorType.Humidity:
upperBound = HUMIDITY_UPPER_BOUND;
lowerBound = HUMIDITY_LOWER_BOUND;
break;
case CaptorType.Luminosity:
upperBound = LUMINOSITY_UPPER_BOUND;
lowerBound = LUMINOSITY_LOWER_BOUND;
break;
case CaptorType.Temperature:
upperBound = TEMPERATURE_UPPER_BOUND;
lowerBound = TEMPERATURE_LOWER_BOUND;
break;
case CaptorType.Tank:
upperBound = TANK_UPPER_BOUND;
lowerBound = TANK_LOWER_BOUND;
break;
default:
throw new InvalidOperationException("How did you just got here?");
}
return (d - lowerBound) / (upperBound - lowerBound);
}
throw new ArgumentException("Invalid source or argument");
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,29 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace PlantBox.Client.Converters
{
class JsonCultureInfoConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(CultureInfo) ? true : false;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var c = new CultureInfo((string)reader.Value);
return c;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var culture = (CultureInfo)value;
writer.WriteValue(culture.Name);
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;
namespace PlantBox.Client.Extensions
{
public static class ColorExtensions
{
public static string GetHexString(this Color color)
{
var red = (int)(color.R * 255);
var green = (int)(color.G * 255);
var blue = (int)(color.B * 255);
var alpha = (int)(color.A * 255);
var hex = $"#{alpha:X2}{red:X2}{green:X2}{blue:X2}";
return hex;
}
}
}

View File

@@ -23,7 +23,7 @@ namespace PlantBox.Client.Extensions
#if DEBUG
System.Diagnostics.Debug.WriteLine($"[ImageResourceExtension](Error): File '{name}' does not exist");
#else
throw new ArgumentException($"File '{Path}' does not exist");
throw new ArgumentException($"File '{path}' does not exist");
#endif
}

View File

@@ -22,7 +22,7 @@ namespace PlantBox.Client.Extensions
public object ProvideValue(IServiceProvider serviceProvider)
{
return _resourceManager.GetString(Name) ?? $"#{Name}";
return _resourceManager.GetString(Name, Locale.Culture) ?? $"#{Name}";
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Client.Extensions
{
// Source: https://stackoverflow.com/a/14285561
public static class TimeSpanExtensions
{
/// <summary>
/// Multiplies a timespan by an integer value
/// </summary>
public static TimeSpan Multiply(this TimeSpan multiplicand, int multiplier)
{
return TimeSpan.FromTicks(multiplicand.Ticks * multiplier);
}
/// <summary>
/// Multiplies a timespan by a double value
/// </summary>
public static TimeSpan Multiply(this TimeSpan multiplicand, double multiplier)
{
return TimeSpan.FromTicks((long)(multiplicand.Ticks * multiplier));
}
}
}

View File

@@ -3,12 +3,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:extensions="clr-namespace:PlantBox.Client.Extensions"
xmlns:models="clr-namespace:PlantBox.Client.Models"
x:Class="PlantBox.Client.Forms.AboutPage"
Padding="{OnPlatform UWP=10}">
x:Class="PlantBox.Client.Forms.AboutPage">
<ContentPage.Content>
<ListView x:Name="listView"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
SelectionMode="None"
IsGroupingEnabled="True"
GroupDisplayBinding="{Binding Title}"

View File

@@ -28,7 +28,13 @@ namespace PlantBox.Client.Forms
},
new AboutPageItemGroup(Locale.Libraries)
{
new AboutPageItem("Microcharts", "https://github.com/aloisdeniel/Microcharts", true)
new AboutPageItem("Microcharts", "https://github.com/aloisdeniel/Microcharts", true),
new AboutPageItem("Newtonsoft.Json", "https://github.com/JamesNK/Newtonsoft.Json", true)
},
new AboutPageItemGroup(Locale.Media)
{
new AboutPageItem("Android Material Icons", "https://material.io/tools/icons/", true),
new AboutPageItem("Android Asset Studio", "https://romannurik.github.io/AndroidAssetStudio/", true)
}
};
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PlantBox.Client.Forms.Controls.StatusBar.StatusBar">
<ContentView.Content>
<StackLayout HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<Label x:Name="label"
HorizontalOptions="CenterAndExpand"
VerticalOptions="Start"
FontSize="Medium" />
<AbsoluteLayout FlexLayout.Grow="1"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<BoxView x:Name="BackgroundBar"
AbsoluteLayout.LayoutBounds="0, 1, 1, 1"
AbsoluteLayout.LayoutFlags="All" />
<BoxView x:Name="ForegroundBar"
AbsoluteLayout.LayoutBounds="0, 1, 1, .5"
AbsoluteLayout.LayoutFlags="All" />
<BoxView x:Name="UpperBoundBar"
AbsoluteLayout.LayoutBounds="0, 0.2, 1, 5" />
<BoxView x:Name="LowerBoundBar"
AbsoluteLayout.LayoutBounds="0, 0.8, 1, 5" />
</AbsoluteLayout>
<Image x:Name="image"
HorizontalOptions="CenterAndExpand"
VerticalOptions="End"
Margin="2"/>
</StackLayout>
</ContentView.Content>
</ContentView>

View File

@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PlantBox.Client.Forms.Controls.StatusBar
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class StatusBar : ContentView
{
// Progress
public static readonly BindableProperty ProgressProperty = BindableProperty.Create(nameof(Progress), typeof(double), typeof(StatusBar), 0.5);
public double Progress
{
get => (double)GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
// HintColor
public static readonly BindableProperty HintColorProperty = BindableProperty.Create(nameof(HintColor), typeof(Color), typeof(StatusBar));
public Color HintColor
{
get => (Color)GetValue(HintColorProperty);
set => SetValue(HintColorProperty, value);
}
// ForegroundColor
public static readonly BindableProperty ForegroundColorProperty = BindableProperty.Create(nameof(ForegroundColor), typeof(Color), typeof(StatusBar));
public Color ForegroundColor
{
get => (Color)GetValue(ForegroundColorProperty);
set => SetValue(ForegroundColorProperty, value);
}
// Image
public static readonly BindableProperty ImageProperty = BindableProperty.Create(nameof(Image), typeof(ImageSource), typeof(StatusBar));
public ImageSource Image
{
get => (ImageSource)GetValue(ImageProperty);
set => SetValue(ImageProperty, value);
}
// Label
public static readonly BindableProperty LabelProperty = BindableProperty.Create(nameof(Label), typeof(string), typeof(StatusBar));
public string Label
{
get => (string)GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
// BoundHeight
public static readonly BindableProperty BoundHeightProperty = BindableProperty.Create(nameof(BoundHeight), typeof(double), typeof(StatusBar));
public double BoundHeight
{
get => (double)GetValue(BoundHeightProperty);
set => SetValue(BoundHeightProperty, value);
}
// BoundColor
public static readonly BindableProperty BoundColorProperty = BindableProperty.Create(nameof(BoundColor), typeof(Color), typeof(StatusBar));
public Color BoundColor
{
get => (Color)GetValue(BoundColorProperty);
set => SetValue(BoundColorProperty, value);
}
// LowerBound
public static readonly BindableProperty LowerBoundProperty = BindableProperty.Create(nameof(LowerBound), typeof(double), typeof(StatusBar), 0.2);
public double LowerBound
{
get => (double)GetValue(LowerBoundProperty);
set => SetValue(LowerBoundProperty, value);
}
// UpperBound
public static readonly BindableProperty UpperBoundProperty = BindableProperty.Create(nameof(UpperBound), typeof(double), typeof(StatusBar), 0.8);
public double UpperBound
{
get => (double)GetValue(UpperBoundProperty);
set => SetValue(UpperBoundProperty, value);
}
public StatusBar()
{
InitializeComponent();
AbsoluteLayout.SetLayoutFlags(UpperBoundBar, AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
AbsoluteLayout.SetLayoutFlags(LowerBoundBar, AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
}
protected override void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == ProgressProperty.PropertyName)
{
new Animation
(
x => AbsoluteLayout.SetLayoutBounds(ForegroundBar, new Rectangle(0, 1, 1, x)),
AbsoluteLayout.GetLayoutBounds(ForegroundBar).Height,
Progress,
Easing.CubicInOut
).Commit(this, "Scale", 16, 1000);
}
if (propertyName == HintColorProperty.PropertyName)
{
BackgroundBar.BackgroundColor = HintColor;
}
if (propertyName == ForegroundColorProperty.PropertyName)
{
ForegroundBar.BackgroundColor = ForegroundColor;
label.TextColor = ForegroundColor;
}
if (propertyName == ImageProperty.PropertyName)
{
image.Source = Image;
}
if (propertyName == LabelProperty.PropertyName)
{
label.Text = Label;
}
if (propertyName == BoundHeightProperty.PropertyName || propertyName == LowerBoundProperty.PropertyName || propertyName == UpperBoundProperty.PropertyName)
{
// Y axis is reversed
double realBoundHeight = BoundHeight / BackgroundBar.Height;
AbsoluteLayout.SetLayoutBounds(LowerBoundBar, new Rectangle(0, 1 - LowerBound, 1, BoundHeight));
AbsoluteLayout.SetLayoutBounds(UpperBoundBar, new Rectangle(0, 1 - UpperBound, 1, BoundHeight));
}
if (propertyName == BoundColorProperty.PropertyName)
{
LowerBoundBar.BackgroundColor = BoundColor;
UpperBoundBar.BackgroundColor = BoundColor;
}
}
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:extensions="clr-namespace:PlantBox.Client.Extensions"
x:Class="PlantBox.Client.Forms.HomeItem">
<ContentView.Content>
<Frame CornerRadius="5"
Padding="5"
BorderColor="{OnPlatform UWP='Gray', Android='Gray'}"
HasShadow="True"
BackgroundColor="{OnPlatform UWP='#F0F0F0'}">
<StackLayout HorizontalOptions="Fill"
VerticalOptions="Fill"
Orientation="Horizontal">
<Image x:Name="imageType"
Source="{extensions:ImageResource Logo_Gray.png}"
WidthRequest="50"
Margin="2, 0, 0, 0"/>
<BoxView VerticalOptions="Fill"
WidthRequest="3"
Margin="0, 2"
CornerRadius="5"
BackgroundColor="Gray" />
<StackLayout Orientation="Vertical">
<Label x:Name="labelName"
VerticalOptions="StartAndExpand"
TextColor="#666666"
Style="{DynamicResource TitleStyle}" />
<Label x:Name="labelType"
VerticalOptions="StartAndExpand"
TextColor="Gray"
Style="{DynamicResource SubtitleStyle}" />
</StackLayout>
<BoxView x:Name="boxState"
VerticalOptions="Fill"
HorizontalOptions="EndAndExpand"
WidthRequest="5"
CornerRadius="5" />
</StackLayout>
</Frame>
</ContentView.Content>
</ContentView>

View File

@@ -0,0 +1,88 @@
using PlantBox.Client.Extensions;
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PlantBox.Client.Forms
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class HomeItem : ContentView
{
// Colors
public static readonly Color BadColor = Color.FromHex("#FA3838");
public static readonly Color GoodColor = Color.FromHex("#8ED959");
public static readonly Color WarningColor = Color.FromHex("#FFC533");
// Name
public static readonly BindableProperty PlantNameProperty = BindableProperty.Create(nameof(PlantName), typeof(string), typeof(HomeItem));
public string PlantName
{
get => (string)GetValue(PlantNameProperty);
set => SetValue(PlantNameProperty, value);
}
// PlantType
public static readonly BindableProperty PlantTypeProperty = BindableProperty.Create(nameof(PlantType), typeof(PlantType), typeof(HomeItem));
public PlantType PlantType
{
get => (PlantType)GetValue(PlantTypeProperty);
set => SetValue(PlantTypeProperty, value);
}
// PlantState
public static readonly BindableProperty PlantStateProperty = BindableProperty.Create(nameof(PlantState), typeof(PlantState), typeof(HomeItem));
public PlantState PlantState
{
get => (PlantState)GetValue(PlantStateProperty);
set => SetValue(PlantStateProperty, value);
}
public HomeItem()
{
InitializeComponent();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == PlantNameProperty.PropertyName)
{
labelName.Text = PlantName;
}
if (propertyName == PlantTypeProperty.PropertyName && PlantType != PlantType.Default)
{
labelType.Text = PlantType.ToString();
// Icon
imageType.Source = ImageResourceExtension.GetImage($"Type.{PlantType}.png");
}
if (propertyName == PlantStateProperty.PropertyName && PlantState != PlantState.Default)
{
Color color;
switch(PlantState)
{
case PlantState.Bad:
color = BadColor;
break;
case PlantState.Warning:
color = WarningColor;
break;
default:
color = GoodColor;
break;
}
boxState.BackgroundColor = color;
}
}
}
}

View File

@@ -2,16 +2,43 @@
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:extensions="clr-namespace:PlantBox.Client.Extensions"
xmlns:forms="clr-namespace:PlantBox.Client.Forms"
xmlns:viewmodels="clr-namespace:PlantBox.Client.ViewModels"
x:Class="PlantBox.Client.Forms.HomePage">
<ContentPage.BindingContext>
<viewmodels:HomeViewModel />
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<Label Text="{extensions:Locale HomePageTitle}"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<Image x:Name="image"
VerticalOptions="CenterAndExpand"
WidthRequest="50"
Source="{extensions:ImageResource Info.png}"/>
</StackLayout>
<AbsoluteLayout>
<!--List-->
<ListView x:Name="List"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All"
ItemsSource="{Binding Plants}"
SelectionMode="None"
RowHeight="{OnPlatform Android=85}"
SeparatorVisibility="None"
IsPullToRefreshEnabled="True"
Refreshing="ListView_Refreshing"
ItemTapped="ListView_ItemTapped">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<forms:HomeItem Margin="5"
PlantName="{Binding Name}"
PlantType="{Binding Type}"
PlantState="{Binding State}" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!--Floating Button-->
<Button AbsoluteLayout.LayoutBounds="0.9, 0.94, 64, 64"
AbsoluteLayout.LayoutFlags="PositionProportional"
BackgroundColor="Accent"
CornerRadius="100"
Image="Add.png"
Clicked="Button_Clicked" />
</AbsoluteLayout>
</ContentPage.Content>
</ContentPage>

View File

@@ -1,8 +1,18 @@
using System;
using PlantBox.Client.Extensions;
using PlantBox.Client.Forms.Plant;
using PlantBox.Client.Models;
using PlantBox.Client.Resources;
using PlantBox.Client.Util;
using PlantBox.Client.ViewModels;
using PlantBox.Shared.Communication;
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Forms;
@@ -17,5 +27,80 @@ namespace PlantBox.Client.Forms
{
InitializeComponent();
}
}
private async void ListView_ItemTapped(object sender, ItemTappedEventArgs e)
{
var plant = (PlantInfo)e.Item;
// Prepare view model
var viewModel = new PlantViewModel(plant);
await Navigation.PushAsync(new PlantPage(viewModel)
{
Title = plant.Name
});
}
protected override bool OnBackButtonPressed()
{
App.MasterPage.IsPresented = true;
return true;
}
private async void Button_Clicked(object sender, EventArgs e)
{
string input = await FormsDialog.InputBox(Navigation, Locale.AddPlantBox, Locale.EnterValidID);
if (ulong.TryParse(input, out ulong id) && await IsValidId(id))
{
App.Settings.IDs.Add(id);
await App.Settings.SaveAsync();
await Refresh();
}
else
{
await DisplayAlert(Locale.Error, Locale.InvalidID, Locale.OK);
}
}
private async Task<bool> IsValidId(ulong id)
{
string plantName = "";
try
{
using (var client = new TcpClient(App.Settings.BrokerIP, Connection.TCP_CLIENT_PORT))
using (var commandStream = new CommandStream(client.GetStream()))
{
await commandStream.SendAsync(new InfoRequest().ToCommandPacket(id));
(_, var info) = await commandStream.ReceiveAsync<InfoResponse>();
plantName = info.Name;
}
}
catch (Exception)
{
}
return plantName != "";
}
private async void ListView_Refreshing(object sender, EventArgs e)
{
await Refresh();
List.EndRefresh();
}
private async Task Refresh()
{
var homeViewModel = (HomeViewModel)BindingContext;
await Task.Run(() => homeViewModel.Plants = homeViewModel.LoadPlants(App.Settings.IDs));
}
}
}

View File

@@ -30,15 +30,18 @@ namespace PlantBox.Client.Forms
if (e.Item is MenuPageItem item)
{
IsPresented = false;
var page = item.PageCreator();
page.Title = item.Title;
var page = item.Page;
if (Detail.Navigation.NavigationStack.Count > 1)
if (Detail.Navigation.NavigationStack.Count > 1 || page == null)
{
await Detail.Navigation.PopToRootAsync();
}
await Detail.Navigation.PushAsync(page);
if (page != null)
{
page.Title = item.Title;
await Detail.Navigation.PushAsync(page);
}
}
}
}

View File

@@ -16,17 +16,17 @@
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal"
HorizontalOptions="FillAndExpand"
VerticalOptions="CenterAndExpand"
Margin="15, 0, 0, 0">
HorizontalOptions="FillAndExpand"
VerticalOptions="CenterAndExpand"
Margin="15, 0, 0, 0">
<Image HeightRequest="32"
WidthRequest="32"
Source="{Binding Image}" />
WidthRequest="32"
Source="{Binding Image}" />
<Label HorizontalOptions="FillAndExpand"
VerticalOptions="CenterAndExpand"
Margin="5, 0, 0, 0"
Style="{DynamicResource SubtitleStyle}"
Text="{Binding Title}" />
VerticalOptions="CenterAndExpand"
Margin="5, 0, 0, 0"
Style="{DynamicResource SubtitleStyle}"
Text="{Binding Title}" />
</StackLayout>
</ViewCell>
</DataTemplate>

View File

@@ -22,8 +22,9 @@ namespace PlantBox.Client.Forms
items.ItemsSource = new MenuPageItem[]
{
new MenuPageItem(Locale.Settings, ImageResourceExtension.GetImage("Settings.png"), typeof(SettingsPage)),
new MenuPageItem(Locale.About, ImageResourceExtension.GetImage("Info.png"), typeof(AboutPage))
new MenuPageItem(Locale.HomePageTitle, ImageResourceExtension.GetImage("Home.png"), null),
new MenuPageItem(Locale.Settings, ImageResourceExtension.GetImage("Settings.png"), new SettingsPage()),
new MenuPageItem(Locale.About, ImageResourceExtension.GetImage("Info.png"), new AboutPage())
};
}
}

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="clr-namespace:PlantBox.Client.Converters"
xmlns:statusbar="clr-namespace:PlantBox.Client.Forms.Controls.StatusBar"
xmlns:models="clr-namespace:PlantBox.Client.Models"
xmlns:viewmodels="clr-namespace:PlantBox.Client.ViewModels"
xmlns:extensions="clr-namespace:PlantBox.Client.Extensions"
x:Class="PlantBox.Client.Forms.Plant.CaptorsPage">
<ContentPage.Resources>
<FlexBasis x:Key="BarWidth">17%</FlexBasis>
<converters:CaptorValueToDoubleConverter x:Key="CaptorConverter" />
</ContentPage.Resources>
<ContentPage.Content>
<FlexLayout Direction="Row"
JustifyContent="SpaceAround"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand"
Margin="20">
<statusbar:StatusBar Progress="{Binding HumidityCaptor.Value, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Humidity}}"
Label="{Binding HumidityCaptor.Value, StringFormat='\{0:F1} %'}"
ForegroundColor="{StaticResource HumidityColor}"
HintColor="{StaticResource HumidityColorSecondary}"
BoundColor="DarkSlateGray"
BoundHeight="2"
LowerBound="{Binding HumidityCaptor.Min, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Humidity}}"
UpperBound="{Binding HumidityCaptor.Max, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Humidity}}"
Image="{extensions:ImageResource Humidity_Icon.png}"
FlexLayout.Basis="{StaticResource BarWidth}" />
<statusbar:StatusBar Progress="{Binding LuminosityCaptor.Value, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Luminosity}}"
Label="{Binding LuminosityCaptor.Value, StringFormat='\{0:F0} lux'}"
ForegroundColor="{StaticResource LuminosityColor}"
HintColor="{StaticResource LuminosityColorSecondary}"
BoundColor="DarkSlateGray"
BoundHeight="2"
LowerBound="{Binding LuminosityCaptor.Min, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Luminosity}}"
UpperBound="{Binding LuminosityCaptor.Max, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Luminosity}}"
Image="{extensions:ImageResource Luminosity_Icon.png}"
FlexLayout.Basis="{StaticResource BarWidth}" />
<statusbar:StatusBar Progress="{Binding TemperatureCaptor.Value, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Temperature}}"
Label="{Binding TemperatureCaptor.Value, StringFormat='\{0:F1} °C'}"
ForegroundColor="{StaticResource TemperatureColor}"
HintColor="{StaticResource TemperatureColorSecondary}"
BoundColor="DarkSlateGray"
BoundHeight="2"
LowerBound="{Binding TemperatureCaptor.Min, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Temperature}}"
UpperBound="{Binding TemperatureCaptor.Max, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Temperature}}"
Image="{extensions:ImageResource Temperature_Icon.png}"
FlexLayout.Basis="{StaticResource BarWidth}" />
<statusbar:StatusBar Progress="{Binding TankValue, Converter={x:StaticResource CaptorConverter}, ConverterParameter={x:Static models:CaptorType.Tank}}"
Label="{Binding TankValue, StringFormat='\{0:F0} %'}"
ForegroundColor="{StaticResource HumidityColor}"
HintColor="{StaticResource HumidityColorSecondary}"
BoundColor="DarkSlateGray"
BoundHeight="2"
LowerBound="-1"
UpperBound="-1"
Image="{extensions:ImageResource Tank_Icon.png}"
FlexLayout.Basis="{StaticResource BarWidth}" />
</FlexLayout>
</ContentPage.Content>
</ContentPage>

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PlantBox.Client.Forms.Plant
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CaptorsPage : ContentPage
{
public CaptorsPage()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:extensions="clr-namespace:PlantBox.Client.Extensions"
xmlns:forms="clr-namespace:Microcharts.Forms;assembly=Microcharts.Forms"
x:Class="PlantBox.Client.Forms.Plant.HistoricPage">
<ContentPage.Content>
<StackLayout Padding="5">
<StackLayout Orientation="Horizontal">
<Label HorizontalOptions="Start"
VerticalOptions="CenterAndExpand"
Text="{extensions:Locale Interval}" />
<Picker x:Name="Picker"
HorizontalOptions="EndAndExpand"
VerticalOptions="CenterAndExpand"
WidthRequest="200"
SelectedItem="{Binding HistoricDuration, Mode=OneWayToSource}"
SelectedIndexChanged="Picker_SelectedIndexChanged" />
</StackLayout>
<ScrollView>
<StackLayout x:Name="layout">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Frame HorizontalOptions="FillAndExpand"
HeightRequest="200"
Padding="5">
<StackLayout>
<Label Text="{Binding Name}" />
<forms:ChartView Chart="{Binding Chart}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
</StackLayout>
</Frame>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</ScrollView>
</StackLayout>
</ContentPage.Content>
</ContentPage>

View File

@@ -0,0 +1,215 @@
using PlantBox.Client.Models;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Microcharts;
using Entry = Microcharts.Entry;
using PlantBox.Client.Extensions;
using PlantBox.Client.ViewModels;
using PlantBox.Client.Resources;
namespace PlantBox.Client.Forms.Plant
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class HistoricPage : ContentPage
{
private readonly SKColor _humidityColor;
private readonly SKColor _humidityColorSecondary;
private readonly SKColor _luminosityColor;
private readonly SKColor _luminosityColorSecondary;
private readonly SKColor _temperatureColor;
private readonly SKColor _temperatureColorSecondary;
public HistoricPage(PlantViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
// Colors
_humidityColor = SKColor.Parse(((Color)Application.Current.Resources["HumidityColor"]).GetHexString());
_humidityColorSecondary = SKColor.Parse(((Color)Application.Current.Resources["HumidityColorSecondary"]).GetHexString());
_luminosityColor = SKColor.Parse(((Color)App.Current.Resources["LuminosityColor"]).GetHexString());
_luminosityColorSecondary = SKColor.Parse(((Color)Application.Current.Resources["LuminosityColorSecondary"]).GetHexString());
_temperatureColor = SKColor.Parse(((Color)Application.Current.Resources["TemperatureColor"]).GetHexString());
_temperatureColorSecondary = SKColor.Parse(((Color)Application.Current.Resources["TemperatureColorSecondary"]).GetHexString());
// Picker
var durations = new List<Duration>()
{
new Duration(1, Interval.Hourly),
new Duration(1, Interval.Daily),
new Duration(2, Interval.Daily),
new Duration(1, Interval.Weekly),
new Duration(2, Interval.Monthly),
new Duration(3, Interval.Monthly),
new Duration(6, Interval.Monthly),
new Duration(1, Interval.Yearly),
new Duration(2, Interval.Yearly),
new Duration(3, Interval.Yearly)
};
Picker.ItemsSource = durations;
Picker.SelectedIndex = 0;
}
private void Picker_SelectedIndexChanged(object sender, EventArgs e)
{
DisplayCharts();
}
private void DisplayCharts()
{
var charts = new NamedChart[]
{
new NamedChart(CreateChart(CaptorType.Humidity, _humidityColor, _humidityColorSecondary), Locale.Humidity),
new NamedChart(CreateChart(CaptorType.Luminosity, _luminosityColor, _luminosityColorSecondary), Locale.Luminosity),
new NamedChart(CreateChart(CaptorType.Temperature, _temperatureColor, _temperatureColorSecondary), Locale.Temperature)
};
BindableLayout.SetItemsSource(layout, charts);
}
private Chart CreateChart(CaptorType captorType, SKColor begin, SKColor end)
{
var chart = new LineChart()
{
LabelTextSize = 14,
Margin = 10,
LineAreaAlpha = 50,
LineMode = LineMode.Straight
};
var entries = CreateEntries(captorType, begin, end);
chart.Entries = entries;
return chart;
}
private Entry[] CreateEntries(CaptorType captorType, SKColor begin, SKColor end)
{
PlantViewModel viewModel = (PlantViewModel)BindingContext;
Historic historic = viewModel.Historic;
HistoricEntries historicEntries;
uint multiplier = viewModel.HistoricDuration.Multiplier;
uint count;
float step;
string labelFormat;
string valueFormat;
switch (viewModel.HistoricDuration.Interval)
{
case Interval.Hourly:
historicEntries = historic.MinutelyHistoric;
count = 12;
step = 2;
labelFormat = "HH:mm";
break;
case Interval.Daily:
historicEntries = historic.HourlyHistoric;
count = 24;
step = 3;
labelFormat = "HH:mm";
break;
case Interval.Weekly:
historicEntries = historic.DailyHistoric;
count = 7;
step = 1;
labelFormat = "ddd";
break;
case Interval.Monthly:
historicEntries = historic.DailyHistoric;
count = 31;
step = 4;
labelFormat = "dd/MM";
break;
case Interval.Yearly:
historicEntries = historic.MonthlyHistoric;
count = 12;
step = multiplier == 1 ? 1 : 1.5f;
labelFormat = multiplier == 1 ? "MMM" : "MM/yy";
break;
default:
throw new InvalidOperationException();
}
count *= multiplier;
step *= multiplier;
HistoricEntry[] values;
switch (captorType)
{
case CaptorType.Humidity:
values = historicEntries.Humidities;
valueFormat = "{0:F1} %";
break;
case CaptorType.Luminosity:
values = historicEntries.Luminosities;
valueFormat = "{0:F0} lux";
break;
case CaptorType.Temperature:
values = historicEntries.Temperatures;
valueFormat = "{0:F1} °C";
break;
default:
throw new InvalidOperationException();
}
// Take values
values = values.Reverse().Take((int)count).ToArray();
Entry[] entries = new Entry[values.Length / (int)step];
SKColor[] colors = CreateGradient(begin, end, values.Length / (int)step).Reverse().ToArray();
int index = 0;
for (uint i = 0; index < entries.Length; i += (uint)step)
{
var entry = values[i];
double value = entry.Value != 0 ? entry.Value : 0.0001;
entries[index] = new Entry((float)value)
{
Label = entry.Date.ToString(labelFormat),
ValueLabel = string.Format(valueFormat, value),
Color = colors[index]
};
index++;
}
return entries.Reverse().ToArray();
}
private SKColor[] CreateGradient(SKColor begin, SKColor end, int count)
{
var colors = new SKColor[count];
byte currentRed = begin.Red;
byte deltaRed = (byte)((end.Red - begin.Red) / count);
byte currentGreen = begin.Green;
byte deltaGreen = (byte)((end.Green - begin.Green) / count);
byte currentBlue = begin.Blue;
byte deltaBlue = (byte)((end.Blue - begin.Blue) / count);
for (int i = 0; i < count; i++)
{
colors[i] = new SKColor(currentRed, currentGreen, currentBlue);
currentRed += deltaRed;
currentGreen += deltaGreen;
currentBlue += deltaBlue;
}
return colors;
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PlantBox.Client.Forms.Plant.PlantPage" />

View File

@@ -0,0 +1,33 @@
using PlantBox.Client.Models;
using PlantBox.Client.Resources;
using PlantBox.Client.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PlantBox.Client.Forms.Plant
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class PlantPage : TabbedPage
{
public PlantPage(PlantViewModel viewModel)
{
InitializeComponent();
Children.Add(new CaptorsPage()
{
BindingContext = viewModel,
Title = Locale.Informations
});
Children.Add(new HistoricPage(viewModel)
{
Title = Locale.Historic
});
}
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PlantBox.Client.Forms.Settings.LanguageSelectorPage">
<ContentPage.Content>
<ListView x:Name="listView"
SelectionMode="None"
ItemTapped="ListView_ItemTapped">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding NativeName}"
Detail="{Binding Name}"
TextColor="Accent" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
</ContentPage>

View File

@@ -0,0 +1,45 @@
using PlantBox.Client.Resources;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PlantBox.Client.Forms.Settings
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class LanguageSelectorPage : ContentPage
{
private CultureInfo[] _supportedCultures = new CultureInfo[]
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR")
};
public LanguageSelectorPage()
{
InitializeComponent();
listView.ItemsSource = _supportedCultures;
}
private async void ListView_ItemTapped(object sender, ItemTappedEventArgs e)
{
if (e.Item is CultureInfo culture)
{
App.Settings.Language = culture;
Locale.Culture = culture;
await App.Settings.SaveAsync();
(Application.Current as App).MainPage = new MainPage();
//await DisplayAlert(Locale.LanguageChangedTitle, Locale.LanguageChangedMessage, "Ok");
//await Navigation.PopAsync();
}
}
}
}

View File

@@ -1,12 +1,35 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PlantBox.Client.Forms.Settings.SettingsPage">
xmlns:extensions="clr-namespace:PlantBox.Client.Extensions"
x:Class="PlantBox.Client.Forms.Settings.SettingsPage"
BindingContext="{StaticResource Settings}">
<ContentPage.Resources>
<Style TargetType="TextCell">
<Setter x:Name="TextCellStyle" Property="TextColor" Value="Green" />
</Style>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout>
<Label Text="Welcome to Xamarin.Forms!"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
<TableView x:Name="table" Intent="Settings">
<TableRoot>
<TableSection Title="{extensions:Locale General}">
<TextCell Text="{extensions:Locale LanguageSetting}"
Detail="{extensions:Locale LanguageSettingDetail}"
Tapped="Language_Tapped" />
</TableSection>
<TableSection Title="{extensions:Locale Notifications}">
<SwitchCell Text="{extensions:Locale NotificationsDisabled}"
On="{Binding NotificationsDisabled}" />
<SwitchCell Text="{extensions:Locale NotificationsMuted}"
On="{Binding NotificationsMuted}" />
</TableSection>
<TableSection Title="{extensions:Locale Server}">
<EntryCell Label="IP: "
LabelColor="Accent"
Placeholder="ip"
Text="{Binding BrokerIP}"/>
</TableSection>
</TableRoot>
</TableView>
</ContentPage.Content>
</ContentPage>

View File

@@ -1,5 +1,7 @@
using System;
using PlantBox.Client.Resources;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -12,9 +14,38 @@ namespace PlantBox.Client.Forms.Settings
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class SettingsPage : ContentPage
{
public SettingsPage ()
public SettingsPage()
{
InitializeComponent ();
InitializeComponent();
// Save on close
Disappearing += async (s, e) => await App.Settings.SaveAsync();
// Can't apply style to cells, so...
ApplyStyle();
}
}
private void ApplyStyle()
{
foreach (var cell in table.Root.SelectMany(x => x))
{
if (cell is TextCell textCell)
{
if (Device.RuntimePlatform == Device.UWP)
{
textCell.TextColor = Color.Accent;
}
}
else if (cell is SwitchCell switchCell)
{
switchCell.Tapped += (s, e) => (s as SwitchCell).On = !(s as SwitchCell).On;
}
}
}
private async void Language_Tapped(object sender, EventArgs e)
{
await Navigation.PushAsync(new LanguageSelectorPage());
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Client.Models
{
enum CaptorType
{
Humidity,
Luminosity,
Tank,
Temperature
}
}

View File

@@ -0,0 +1,43 @@
using PlantBox.Client.Resources;
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Client.Models
{
public class Duration
{
public uint Multiplier { get; }
public Interval Interval { get; }
public Duration(uint multiplier, Interval interval)
{
Multiplier = multiplier;
Interval = interval;
}
private string IntervalToString(Interval interval)
{
switch (interval)
{
case Interval.Hourly:
return Locale.Hour;
case Interval.Daily:
return Locale.Day;
case Interval.Weekly:
return Locale.Week;
case Interval.Monthly:
return Locale.Month;
case Interval.Yearly:
return Locale.Year;
default:
return "Invalid interval";
}
}
public override string ToString()
{
return $"{Multiplier} {IntervalToString(Interval)}{(Multiplier > 1 && !IntervalToString(Interval).EndsWith("s") ? "s" : "")}";
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Client.Models
{
public class Historic
{
public HistoricEntries MinutelyHistoric { get; }
public HistoricEntries HourlyHistoric { get; }
public HistoricEntries DailyHistoric { get; }
public HistoricEntries MonthlyHistoric { get; }
public Historic
(
HistoricEntries minutelyHistoric,
HistoricEntries hourlyHistoric,
HistoricEntries dailyHistoric,
HistoricEntries monthlyHistoric
)
{
MinutelyHistoric = minutelyHistoric;
HourlyHistoric = hourlyHistoric;
DailyHistoric = dailyHistoric;
MonthlyHistoric = monthlyHistoric;
}
}
}

View File

@@ -0,0 +1,48 @@
using PlantBox.Client.Extensions;
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PlantBox.Client.Models
{
public class HistoricEntries
{
public DateTime LastTime { get; }
public TimeSpan TimeInterval { get; }
public HistoricEntry[] Humidities { get; }
public HistoricEntry[] Luminosities { get; }
public HistoricEntry[] Temperatures { get; }
public DateTime EndTime => LastTime - new TimeSpan(TimeInterval.Ticks * Count);
public int Count => Humidities.Length;
public HistoricEntries(DateTime lastTime, TimeSpan timeInterval, double[] humidities, double[] luminosities, double[] temperatures)
{
LastTime = lastTime;
TimeInterval = timeInterval;
Humidities = ValuesToEntries(humidities);
Luminosities = ValuesToEntries(luminosities);
Temperatures = ValuesToEntries(temperatures);
}
public HistoricEntries(HistoricResponse response, TimeSpan time) : this(DateTime.Now - response.Time, time, response.Humidities, response.Luminosities, response.Temperatures)
{
}
private HistoricEntry[] ValuesToEntries(double[] values)
{
HistoricEntry[] entries = new HistoricEntry[values.Length];
var oldestDate = LastTime - TimeInterval.Multiply(values.Length - 1);
for (int i = 0; i < values.Length; i++)
{
entries[i] = new HistoricEntry(oldestDate + TimeInterval.Multiply(i), values[i]);
}
return entries;
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Client.Models
{
public class HistoricEntry
{
public DateTime Date { get; }
public double Value { get; }
public HistoricEntry(DateTime date, double value)
{
Date = date;
Value = value;
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Client.Models
{
public enum Interval
{
Hourly,
Daily,
Weekly,
Monthly,
Yearly
}
}

View File

@@ -11,41 +11,15 @@ namespace PlantBox.Client.Models
{
public event PropertyChangedEventHandler PropertyChanged;
private ImageSource _image;
public ImageSource Image
{
get => _image;
set
{
if (value != _image)
{
_image = value;
OnPropertyChanged(nameof(Image));
}
}
}
public ImageSource Image { get; }
public string Title { get; }
public Page Page { get; }
private string _title;
public string Title
public MenuPageItem(string title, ImageSource image, Page targetPage)
{
get => _title;
set
{
if (value != _title)
{
_title = value;
OnPropertyChanged(nameof(Title));
}
}
}
public Func<Page> PageCreator { get; }
public MenuPageItem(string title, ImageSource image, Type targetPage)
{
_image = image;
_title = title;
PageCreator = () => (Page)Activator.CreateInstance(targetPage);
Image = image;
Title = title;
Page = targetPage;
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)

View File

@@ -0,0 +1,19 @@
using Microcharts;
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Client.Models
{
public class NamedChart
{
public Chart Chart { get; }
public string Name { get; }
public NamedChart(Chart chart, string name)
{
Chart = chart;
Name = name;
}
}
}

View File

@@ -0,0 +1,35 @@
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Client.Models
{
public class PlantInfo
{
public string Name { get; }
public ulong ID { get; }
public PlantType Type { get; }
public PlantState State { get; }
public double HumidityMin { get; }
public double HumidityMax { get; }
public double LuminosityMin { get; }
public double LuminosityMax { get; }
public double TemperatureMin { get; }
public double TemperatureMax { get; }
public PlantInfo(string name, ulong id, PlantType type, PlantState state, double humidityMin, double humidityMax, double luminosityMin, double luminosityMax, double temperatureMin, double temperatureMax)
{
Name = name;
ID = id;
Type = type;
State = state;
HumidityMin = humidityMin;
HumidityMax = humidityMax;
LuminosityMin = luminosityMin;
LuminosityMax = luminosityMax;
TemperatureMin = temperatureMin;
TemperatureMax = temperatureMax;
}
}
}

View File

@@ -0,0 +1,22 @@
using Newtonsoft.Json;
using PlantBox.Client.Converters;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace PlantBox.Client.Models
{
class Settings
{
[JsonConverter(typeof(JsonCultureInfoConverter))]
public CultureInfo Language { get; set; } = CultureInfo.CurrentCulture;
public bool NotificationsDisabled { get; set; } = false;
public bool NotificationsMuted { get; set; } = false;
public string BrokerIP { get; set; } = "";
public List<ulong> IDs { get; set; } = new List<ulong>();
}
}

View File

@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Authors>Ilyx</Authors>
<Version>0.1.0</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -11,23 +12,40 @@
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\Images\Add.png" />
<None Remove="Resources\Images\Banner.png" />
<None Remove="Resources\Images\Help.png" />
<None Remove="Resources\Images\Home.png" />
<None Remove="Resources\Images\Humidity_Icon.png" />
<None Remove="Resources\Images\Info.png" />
<None Remove="Resources\Images\Logo_Gray.png" />
<None Remove="Resources\Images\Luminosity_Icon.png" />
<None Remove="Resources\Images\Settings.png" />
<None Remove="Resources\Images\Tank_Icon.png" />
<None Remove="Resources\Images\Temperature_Icon.png" />
<None Remove="Resources\Images\Type\Ficus.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Images\Add.png" />
<EmbeddedResource Include="Resources\Images\Banner.png" />
<EmbeddedResource Include="Resources\Images\Help.png" />
<EmbeddedResource Include="Resources\Images\Home.png" />
<EmbeddedResource Include="Resources\Images\Humidity_Icon.png" />
<EmbeddedResource Include="Resources\Images\Info.png" />
<EmbeddedResource Include="Resources\Images\Logo_Gray.png" />
<EmbeddedResource Include="Resources\Images\Luminosity_Icon.png" />
<EmbeddedResource Include="Resources\Images\Settings.png" />
<EmbeddedResource Include="Resources\Images\Tank_Icon.png" />
<EmbeddedResource Include="Resources\Images\Temperature_Icon.png" />
<EmbeddedResource Include="Resources\Images\Type\Ficus.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Forms" Version="3.4.0.1008975" />
<PackageReference Include="Microcharts" Version="0.7.1" />
<PackageReference Include="Microcharts.Forms" Version="0.7.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="Xamarin.Forms" Version="3.6.0.344457" />
</ItemGroup>
<ItemGroup>
@@ -38,6 +56,9 @@
<EmbeddedResource Update="Forms\AboutPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Forms\Controls\StatusBar.xaml">
<Generator>MSBuild:Compile</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Forms\HomePage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
@@ -47,6 +68,21 @@
<EmbeddedResource Update="Forms\MenuPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Forms\HomeItem.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Forms\Plant\CaptorsPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Forms\Plant\HistoricPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Forms\Plant\PlantPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Forms\Settings\LanguageSelectorPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Forms\Settings\SettingsPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
@@ -57,10 +93,9 @@
</ItemGroup>
<ItemGroup>
<Folder Include="ViewModels\" />
</ItemGroup>
<ItemGroup>
<Compile Update="Forms\Settings\LanguageSelectorPage.xaml.cs">
<DependentUpon>LanguageSelectorPage.xaml</DependentUpon>
</Compile>
<Compile Update="Resources\Locale.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 B

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 B

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 B

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -19,7 +19,7 @@ namespace PlantBox.Client.Resources {
// à l'aide d'un outil, tel que ResGen ou Visual Studio.
// Pour ajouter ou supprimer un membre, modifiez votre fichier .ResX, puis réexécutez ResGen
// avec l'option /str ou régénérez votre projet VS.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Locale {
@@ -69,6 +69,15 @@ namespace PlantBox.Client.Resources {
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Add PlantBox.
/// </summary>
internal static string AddPlantBox {
get {
return ResourceManager.GetString("AddPlantBox", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Author.
/// </summary>
@@ -78,6 +87,69 @@ namespace PlantBox.Client.Resources {
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Cancel.
/// </summary>
internal static string Cancel {
get {
return ResourceManager.GetString("Cancel", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Captors.
/// </summary>
internal static string Captors {
get {
return ResourceManager.GetString("Captors", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Day.
/// </summary>
internal static string Day {
get {
return ResourceManager.GetString("Day", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Enter a valid ID:.
/// </summary>
internal static string EnterValidID {
get {
return ResourceManager.GetString("EnterValidID", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Error.
/// </summary>
internal static string Error {
get {
return ResourceManager.GetString("Error", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à General.
/// </summary>
internal static string General {
get {
return ResourceManager.GetString("General", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Historic.
/// </summary>
internal static string Historic {
get {
return ResourceManager.GetString("Historic", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Home.
/// </summary>
@@ -87,6 +159,24 @@ namespace PlantBox.Client.Resources {
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Hour.
/// </summary>
internal static string Hour {
get {
return ResourceManager.GetString("Hour", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Humidity.
/// </summary>
internal static string Humidity {
get {
return ResourceManager.GetString("Humidity", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Informations.
/// </summary>
@@ -96,6 +186,60 @@ namespace PlantBox.Client.Resources {
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Interval:.
/// </summary>
internal static string Interval {
get {
return ResourceManager.GetString("Interval", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Invalid id.
/// </summary>
internal static string InvalidID {
get {
return ResourceManager.GetString("InvalidID", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Changes will take effect at next start.
/// </summary>
internal static string LanguageChangedMessage {
get {
return ResourceManager.GetString("LanguageChangedMessage", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Language changed.
/// </summary>
internal static string LanguageChangedTitle {
get {
return ResourceManager.GetString("LanguageChangedTitle", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Language.
/// </summary>
internal static string LanguageSetting {
get {
return ResourceManager.GetString("LanguageSetting", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Change the language.
/// </summary>
internal static string LanguageSettingDetail {
get {
return ResourceManager.GetString("LanguageSettingDetail", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Libraries.
/// </summary>
@@ -105,6 +249,78 @@ namespace PlantBox.Client.Resources {
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Luminosity.
/// </summary>
internal static string Luminosity {
get {
return ResourceManager.GetString("Luminosity", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Media.
/// </summary>
internal static string Media {
get {
return ResourceManager.GetString("Media", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Month.
/// </summary>
internal static string Month {
get {
return ResourceManager.GetString("Month", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Notifications.
/// </summary>
internal static string Notifications {
get {
return ResourceManager.GetString("Notifications", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Desactivate notifications.
/// </summary>
internal static string NotificationsDisabled {
get {
return ResourceManager.GetString("NotificationsDisabled", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Mute notifications.
/// </summary>
internal static string NotificationsMuted {
get {
return ResourceManager.GetString("NotificationsMuted", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à OK.
/// </summary>
internal static string OK {
get {
return ResourceManager.GetString("OK", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Server.
/// </summary>
internal static string Server {
get {
return ResourceManager.GetString("Server", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Settings.
/// </summary>
@@ -114,6 +330,15 @@ namespace PlantBox.Client.Resources {
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Temperature.
/// </summary>
internal static string Temperature {
get {
return ResourceManager.GetString("Temperature", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Version.
/// </summary>
@@ -122,5 +347,23 @@ namespace PlantBox.Client.Resources {
return ResourceManager.GetString("Version", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Week.
/// </summary>
internal static string Week {
get {
return ResourceManager.GetString("Week", resourceCulture);
}
}
/// <summary>
/// Recherche une chaîne localisée semblable à Year.
/// </summary>
internal static string Year {
get {
return ResourceManager.GetString("Year", resourceCulture);
}
}
}
}

View File

@@ -120,22 +120,103 @@
<data name="About" xml:space="preserve">
<value>À Propos</value>
</data>
<data name="AddPlantBox" xml:space="preserve">
<value>Ajouter un PlantBox</value>
</data>
<data name="Author" xml:space="preserve">
<value>Auteur</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Annuler</value>
</data>
<data name="Captors" xml:space="preserve">
<value>Capteurs</value>
</data>
<data name="Day" xml:space="preserve">
<value>Jour</value>
</data>
<data name="EnterValidID" xml:space="preserve">
<value>Entrer une id valide:</value>
</data>
<data name="Error" xml:space="preserve">
<value>Erreur</value>
</data>
<data name="General" xml:space="preserve">
<value>Général</value>
</data>
<data name="Historic" xml:space="preserve">
<value>Historique</value>
</data>
<data name="HomePageTitle" xml:space="preserve">
<value>Accueil</value>
</data>
<data name="Hour" xml:space="preserve">
<value>Heure</value>
</data>
<data name="Humidity" xml:space="preserve">
<value>Humidité</value>
</data>
<data name="Informations" xml:space="preserve">
<value>Informations</value>
</data>
<data name="Interval" xml:space="preserve">
<value>Intervalle:</value>
</data>
<data name="InvalidID" xml:space="preserve">
<value>Id invalide</value>
</data>
<data name="LanguageChangedMessage" xml:space="preserve">
<value>Les changements prendront effet au prochain lancement</value>
</data>
<data name="LanguageChangedTitle" xml:space="preserve">
<value>Langue modifiée</value>
</data>
<data name="LanguageSetting" xml:space="preserve">
<value>Langue</value>
</data>
<data name="LanguageSettingDetail" xml:space="preserve">
<value>Modifie la langue</value>
</data>
<data name="Libraries" xml:space="preserve">
<value>Bibliothèques</value>
</data>
<data name="Luminosity" xml:space="preserve">
<value>Luminosité</value>
</data>
<data name="Media" xml:space="preserve">
<value>Media</value>
</data>
<data name="Month" xml:space="preserve">
<value>Mois</value>
</data>
<data name="Notifications" xml:space="preserve">
<value>Notifications</value>
</data>
<data name="NotificationsDisabled" xml:space="preserve">
<value>Désactiver les notifications</value>
</data>
<data name="NotificationsMuted" xml:space="preserve">
<value>Rendre muet les notifications</value>
</data>
<data name="OK" xml:space="preserve">
<value>OK</value>
</data>
<data name="Server" xml:space="preserve">
<value>Serveur</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Options</value>
<value>Préférences</value>
</data>
<data name="Temperature" xml:space="preserve">
<value>Température</value>
</data>
<data name="Version" xml:space="preserve">
<value>Version</value>
</data>
<data name="Week" xml:space="preserve">
<value>Semaine</value>
</data>
<data name="Year" xml:space="preserve">
<value>Année</value>
</data>
</root>

View File

@@ -120,22 +120,103 @@
<data name="About" xml:space="preserve">
<value>About</value>
</data>
<data name="AddPlantBox" xml:space="preserve">
<value>Add PlantBox</value>
</data>
<data name="Author" xml:space="preserve">
<value>Author</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Captors" xml:space="preserve">
<value>Captors</value>
</data>
<data name="Day" xml:space="preserve">
<value>Day</value>
</data>
<data name="EnterValidID" xml:space="preserve">
<value>Enter a valid ID:</value>
</data>
<data name="Error" xml:space="preserve">
<value>Error</value>
</data>
<data name="General" xml:space="preserve">
<value>General</value>
</data>
<data name="Historic" xml:space="preserve">
<value>Historic</value>
</data>
<data name="HomePageTitle" xml:space="preserve">
<value>Home</value>
</data>
<data name="Hour" xml:space="preserve">
<value>Hour</value>
</data>
<data name="Humidity" xml:space="preserve">
<value>Humidity</value>
</data>
<data name="Informations" xml:space="preserve">
<value>Informations</value>
</data>
<data name="Interval" xml:space="preserve">
<value>Interval:</value>
</data>
<data name="InvalidID" xml:space="preserve">
<value>Invalid id</value>
</data>
<data name="LanguageChangedMessage" xml:space="preserve">
<value>Changes will take effect at next start</value>
</data>
<data name="LanguageChangedTitle" xml:space="preserve">
<value>Language changed</value>
</data>
<data name="LanguageSetting" xml:space="preserve">
<value>Language</value>
</data>
<data name="LanguageSettingDetail" xml:space="preserve">
<value>Change the language</value>
</data>
<data name="Libraries" xml:space="preserve">
<value>Libraries</value>
</data>
<data name="Luminosity" xml:space="preserve">
<value>Luminosity</value>
</data>
<data name="Media" xml:space="preserve">
<value>Media</value>
</data>
<data name="Month" xml:space="preserve">
<value>Month</value>
</data>
<data name="Notifications" xml:space="preserve">
<value>Notifications</value>
</data>
<data name="NotificationsDisabled" xml:space="preserve">
<value>Desactivate notifications</value>
</data>
<data name="NotificationsMuted" xml:space="preserve">
<value>Mute notifications</value>
</data>
<data name="OK" xml:space="preserve">
<value>OK</value>
</data>
<data name="Server" xml:space="preserve">
<value>Server</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Temperature" xml:space="preserve">
<value>Temperature</value>
</data>
<data name="Version" xml:space="preserve">
<value>Version</value>
</data>
<data name="Week" xml:space="preserve">
<value>Week</value>
</data>
<data name="Year" xml:space="preserve">
<value>Year</value>
</data>
</root>

View File

@@ -0,0 +1,77 @@
using PlantBox.Client.Resources;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace PlantBox.Client.Util
{
class FormsDialog
{
public static Task<string> InputBox(INavigation navigation, string title, string description)
{
// wait in this proc, until user did his input
var tcs = new TaskCompletionSource<string>();
var lblTitle = new Label { Text = title, HorizontalOptions = LayoutOptions.Center, FontAttributes = FontAttributes.Bold };
var lblMessage = new Label { Text = description };
var txtInput = new Entry { Text = "" };
var btnOk = new Button
{
Text = Locale.OK,
WidthRequest = 100,
BackgroundColor = Color.FromRgb(0.8, 0.8, 0.8),
};
btnOk.Clicked += async (s, e) =>
{
// close page
var result = txtInput.Text;
await navigation.PopModalAsync();
// pass result
tcs.SetResult(result);
};
var btnCancel = new Button
{
Text = Locale.Cancel,
WidthRequest = 100,
BackgroundColor = Color.FromRgb(0.8, 0.8, 0.8)
};
btnCancel.Clicked += async (s, e) =>
{
// close page
await navigation.PopModalAsync();
// pass empty result
tcs.SetResult(null);
};
var slButtons = new StackLayout
{
Orientation = StackOrientation.Horizontal,
Children = { btnOk, btnCancel },
};
var layout = new StackLayout
{
Padding = new Thickness(0, 40, 0, 0),
VerticalOptions = LayoutOptions.StartAndExpand,
HorizontalOptions = LayoutOptions.CenterAndExpand,
Orientation = StackOrientation.Vertical,
Children = { lblTitle, lblMessage, txtInput, slButtons },
};
// create and show page
var page = new ContentPage();
page.Content = layout;
navigation.PushModalAsync(page);
// open keyboard
txtInput.Focus();
// code is waiting her, until result is passed with tcs.SetResult() in btn-Clicked
// then proc returns the result
return tcs.Task;
}
}
}

View File

@@ -0,0 +1,84 @@
using PlantBox.Client.Models;
using PlantBox.Shared.Communication;
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace PlantBox.Client.ViewModels
{
class HomeViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<PlantInfo> _plants;
public ObservableCollection<PlantInfo> Plants
{
get => _plants;
set
{
if (value != _plants)
{
_plants = value;
OnPropertyChanged();
}
}
}
public HomeViewModel()
{
Plants = LoadPlants(App.Settings.IDs);
}
public ObservableCollection<PlantInfo> LoadPlants(IEnumerable<ulong> ids)
{
if (App.Settings.BrokerIP == "")
{
return new ObservableCollection<PlantInfo>();
}
IEnumerable<PlantInfo> infos = GetPlantInfos(ids);
return new ObservableCollection<PlantInfo>(infos);
}
private IEnumerable<PlantInfo> GetPlantInfos(IEnumerable<ulong> ids)
{
using (var client = new TcpClient(App.Settings.BrokerIP, Connection.TCP_CLIENT_PORT))
using (var stream = new CommandStream(client.GetStream()))
{
foreach (ulong id in ids)
{
stream.Send(new InfoRequest().ToCommandPacket(id));
(_, var responseInfo) = stream.Receive<InfoResponse>();
yield return new PlantInfo
(
responseInfo.Name,
id,
responseInfo.Type,
responseInfo.State,
responseInfo.HumidityMin,
responseInfo.HumidityMax,
responseInfo.LuminosityMin,
responseInfo.LuminosityMax,
responseInfo.TemperatureMin,
responseInfo.TemperatureMax
);
}
}
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,181 @@
using PlantBox.Client.Models;
using PlantBox.Shared.Communication;
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PlantBox.Client.ViewModels
{
public class PlantViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public PlantViewModel(PlantInfo plant)
{
PlantInfo = plant;
LoadValues();
}
private Historic _historic;
public Historic Historic
{
get => _historic;
set
{
if (value != _historic)
{
_historic = value;
OnPropertyChanged();
}
}
}
private PlantInfo _plantInfo;
public PlantInfo PlantInfo
{
get => _plantInfo;
set
{
if (value != _plantInfo)
{
_plantInfo = value;
OnPropertyChanged();
}
}
}
private CaptorValue _humidityCaptor;
public CaptorValue HumidityCaptor
{
get => _humidityCaptor;
set
{
if (value != _humidityCaptor)
{
_humidityCaptor = value;
OnPropertyChanged();
}
}
}
private CaptorValue _luminosityCaptor;
public CaptorValue LuminosityCaptor
{
get => _luminosityCaptor;
set
{
if (value != _luminosityCaptor)
{
_luminosityCaptor = value;
OnPropertyChanged();
}
}
}
private CaptorValue _temperatureCaptor;
public CaptorValue TemperatureCaptor
{
get => _temperatureCaptor;
set
{
if (value != _temperatureCaptor)
{
_temperatureCaptor = value;
OnPropertyChanged();
}
}
}
private double _tankValue;
public double TankValue
{
get => _tankValue;
set
{
if (value != _tankValue)
{
_tankValue = value;
OnPropertyChanged();
}
}
}
private Duration _historicDuration;
public Duration HistoricDuration
{
get => _historicDuration;
set
{
if (value != _historicDuration)
{
_historicDuration = value;
OnPropertyChanged();
}
}
}
private void LoadValues()
{
CaptorValue humidityCaptor;
CaptorValue luminosityCaptor;
CaptorValue temperatureCaptor;
double tankValue;
HistoricResponse minutelyHistoric;
HistoricResponse hourlyHistoric;
HistoricResponse dailyHistoric;
HistoricResponse monthlyHistoric;
using (var client = new TcpClient(App.Settings.BrokerIP, Connection.TCP_CLIENT_PORT))
using (var stream = new CommandStream(client.GetStream()))
{
// Captors info
stream.Send(new CaptorsRequest().ToCommandPacket(PlantInfo.ID));
(_, var captorsReponse) = stream.Receive<CaptorsResponse>();
humidityCaptor = new CaptorValue(PlantInfo.HumidityMin, PlantInfo.HumidityMax, captorsReponse.Humidity);
luminosityCaptor = new CaptorValue(PlantInfo.LuminosityMin, PlantInfo.LuminosityMax, captorsReponse.Luminosity);
temperatureCaptor = new CaptorValue(PlantInfo.TemperatureMin, PlantInfo.TemperatureMax, captorsReponse.Temperature);
tankValue = captorsReponse.Tank;
// Historic
stream.Send(new HistoricRequest(HistoricInterval.Minutely, 12).ToCommandPacket(PlantInfo.ID));
(_, minutelyHistoric) = stream.Receive<HistoricResponse>();
stream.Send(new HistoricRequest(HistoricInterval.Hourly, 48).ToCommandPacket(PlantInfo.ID));
(_, hourlyHistoric) = stream.Receive<HistoricResponse>();
stream.Send(new HistoricRequest(HistoricInterval.Daily, 186).ToCommandPacket(PlantInfo.ID));
(_, dailyHistoric) = stream.Receive<HistoricResponse>();
stream.Send(new HistoricRequest(HistoricInterval.Monthly, 36).ToCommandPacket(PlantInfo.ID));
(_, monthlyHistoric) = stream.Receive<HistoricResponse>();
}
HumidityCaptor = humidityCaptor;
LuminosityCaptor = luminosityCaptor;
TemperatureCaptor = temperatureCaptor;
TankValue = tankValue;
var historic = new Historic
(
new HistoricEntries(minutelyHistoric, TimeSpan.FromMinutes(5)),
new HistoricEntries(hourlyHistoric, TimeSpan.FromHours(1)),
new HistoricEntries(dailyHistoric, TimeSpan.FromDays(1)),
new HistoricEntries(monthlyHistoric, TimeSpan.FromDays(31))
);
Historic = historic;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,120 @@
using Newtonsoft.Json;
using PlantBox.Client.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace PlantBox.Client.ViewModels
{
public class SettingsViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public const string FileName = "Settings.json";
public string FilePath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), FileName);
private Settings _settings;
// Settings
public CultureInfo Language
{
get => _settings.Language;
set
{
if (value != _settings.Language)
{
_settings.Language = value;
OnPropertyChanged();
}
}
}
public bool NotificationsDisabled
{
get => _settings.NotificationsDisabled;
set
{
if (value != _settings.NotificationsDisabled)
{
_settings.NotificationsDisabled = value;
OnPropertyChanged();
}
}
}
public bool NotificationsMuted
{
get => _settings.NotificationsMuted;
set
{
if (value != _settings.NotificationsMuted)
{
_settings.NotificationsMuted = value;
OnPropertyChanged();
}
}
}
public string BrokerIP
{
get => _settings.BrokerIP;
set
{
if (value != _settings.BrokerIP)
{
_settings.BrokerIP = value;
OnPropertyChanged();
}
}
}
public List<ulong> IDs
{
get => _settings.IDs;
set
{
if (value != _settings.IDs)
{
_settings.IDs = value;
OnPropertyChanged();
}
}
}
public SettingsViewModel()
{
if (File.Exists(FilePath))
{
_settings = JsonConvert.DeserializeObject<Settings>(File.ReadAllText(FilePath));
}
else
{
_settings = new Settings();
}
}
public void Save()
{
using (var file = new StreamWriter(FilePath, false))
{
file.WriteLine(JsonConvert.SerializeObject(_settings));
}
}
public async Task SaveAsync()
{
using (var file = new StreamWriter(FilePath, false))
{
await file.WriteLineAsync(JsonConvert.SerializeObject(_settings));
}
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Shared.Communication
{
/// <summary>
/// All valid commands as described in the Wiki > Commands
/// </summary>
public enum Command
{
Captors,
Historic,
Info,
Invalid,
Ping
}
}

View File

@@ -0,0 +1,60 @@
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PlantBox.Shared.Communication
{
/// <summary>
/// Represent a Packet as described in the Wiki > Protocol
/// </summary>
public class CommandPacket
{
/// <summary>
/// A valid packet command, see the Wiki > Commands
/// </summary>
public Command Command { get; }
/// <summary>
/// A valid id
/// </summary>
public ulong ID { get; }
/// <summary>
/// Any arguments, see the Wiki page of a command for more infos
/// </summary>
public string[] Arguments { get; }
/// <summary>
/// Create a <see cref="CommandPacket"/>
/// </summary>
/// <param name="command"></param>
/// <param name="id"></param>
/// <param name="arguments"></param>
public CommandPacket(Command command, ulong id, string[] arguments)
{
Command = command;
ID = id;
Arguments = arguments ?? Array.Empty<string>();
}
/// <summary>
/// Create a <see cref="CommandPacket"/>
/// </summary>
/// <param name="command"></param>
/// <param name="arguments"></param>
public CommandPacket(Command command, params string[] arguments)
{
Command = command;
ID = ulong.Parse(arguments[0]);
Arguments = arguments != null && arguments.Length > 1 ? arguments.Skip(1).ToArray() : Array.Empty<string>();
}
/// <summary>
/// Convert a <see cref="CommandPacket"/> to a valid text representation of a packet, see Wiki > Protocol
/// </summary>
/// <returns>A valid packet text representation, ready to be sent</returns>
public override string ToString()
{
return $"{Command.ToString().ToUpperInvariant()}\n{ID}{(Arguments.Length > 0 ? $";{string.Join(";", Arguments)}" : string.Empty)}\n";
}
}
}

View File

@@ -0,0 +1,181 @@
using PlantBox.Shared.Communication.Commands;
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace PlantBox.Shared.Communication
{
/// <summary>
/// Wrap a <see cref="NetworkStream"/> to send <see cref="CommandPacket"/> easily, see Wiki > Protocol
/// </summary>
public class CommandStream : IDisposable
{
private NetworkStream _stream;
/// <summary>
/// Create a new <see cref="CommandStream"/>
/// </summary>
/// <param name="networkStream">A connected and valid <see cref="NetworkStream"/></param>
public CommandStream(NetworkStream networkStream)
{
_stream = networkStream;
}
/// <summary>
/// Release resources associated with the underlying stream
/// </summary>
public void Dispose()
{
_stream.Dispose();
}
/// <summary>
/// Read a <see cref="CommandPacket"/> from the stream
/// </summary>
/// <returns></returns>
public CommandPacket Receive()
{
// Length
byte[] buffer = new byte[4];
_stream.Read(buffer, 0, buffer.Length);
uint length = BitConverter.ToUInt32(buffer, 0);
// Data
buffer = new byte[length];
int position = 0;
while (position < length)
{
int bytesToRead = Math.Min(Connection.BUFFER_SIZE, (int)length - position);
int bytesRead = _stream.Read(buffer, position, bytesToRead);
position += bytesRead;
}
string[] packet = Encoding.UTF8.GetString(buffer).Split('\n');
if (!Enum.TryParse(packet[0], true, out Command command))
{
command = Command.Invalid;
}
return new CommandPacket(command, packet[1].Split(';'));
}
/// <summary>
/// Read a <see cref="CommandPacket"/> from the stream and deserialize data to a <see cref="CommandSerializable{T}"/>
/// </summary>
/// <typeparam name="T">A valid <see cref="CommandSerializable{T}"/></typeparam>
/// <returns></returns>
public (CommandPacket, T) Receive<T>() where T : CommandSerializable<T>, new()
{
var packet = Receive();
return (packet, new T().Deserialize(packet.Arguments));
}
/// <summary>
/// Read a <see cref="CommandPacket"/> from the stream asynchronously
/// </summary>
/// <returns></returns>
public async Task<CommandPacket> ReceiveAsync()
{
// Length
byte[] buffer = new byte[4];
await _stream.ReadAsync(buffer, 0, buffer.Length);
uint length = BitConverter.ToUInt32(buffer, 0);
// Data
buffer = new byte[length];
int position = 0;
while (position < length)
{
int bytesToRead = Math.Min(Connection.BUFFER_SIZE, (int)length - position);
int bytesRead = await _stream.ReadAsync(buffer, position, bytesToRead);
position += bytesRead;
}
string[] packet = Encoding.UTF8.GetString(buffer).Split('\n');
if (!Enum.TryParse(packet[0], true, out Command command))
{
command = Command.Invalid;
}
return new CommandPacket(command, packet[1].Split(';'));
}
/// <summary>
/// Read a <see cref="CommandPacket"/> from the stream and deserialize data to a <see cref="CommandSerializable{T}"/> asynchronously
/// </summary>
/// <typeparam name="T">A valid <see cref="CommandSerializable{T}"/></typeparam>
/// <returns></returns>
public async Task<(CommandPacket, T)> ReceiveAsync<T>() where T : CommandSerializable<T>, new()
{
var packet = await ReceiveAsync();
return (packet, new T().Deserialize(packet.Arguments));
}
/// <summary>
/// Write a <see cref="CommandPacket"/> in the stream
/// </summary>
/// <param name="command"></param>
public void Send(CommandPacket command)
{
string packet = command.ToString();
byte[] data = Encoding.UTF8.GetBytes(packet);
uint length = (uint)data.Length;
// Length
_stream.Write(BitConverter.GetBytes(length), 0, 4);
// Data
int position = 0;
while (position < length)
{
int bytesToWrite = Math.Min(Connection.BUFFER_SIZE, (int)length - position);
_stream.Write(data, position, bytesToWrite);
position += bytesToWrite;
}
}
/// <summary>
/// Write a <see cref="CommandPacket"/> in the stream asynchronously
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
public async Task SendAsync(CommandPacket command)
{
string packet = command.ToString();
byte[] data = Encoding.UTF8.GetBytes(packet);
uint length = (uint)data.Length;
// Length
await _stream.WriteAsync(BitConverter.GetBytes(length), 0, 4);
// Data
int position = 0;
while (position < length)
{
int bytesToWrite = Math.Min(Connection.BUFFER_SIZE, (int)length - position);
await _stream.WriteAsync(data, position, bytesToWrite);
position += bytesToWrite;
}
}
}
}

View File

@@ -0,0 +1,45 @@
using PlantBox.Shared.Extensions;
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public class CaptorValue
{
public const char ValueSeparator = '/';
public double Min { get; set; }
public double Max { get; set; }
public double Value { get; set; }
public CaptorValue()
{
}
public CaptorValue(string argument)
{
string[] arguments = argument.Split(ValueSeparator);
Min = arguments[0].ToDouble();
Max = arguments[1].ToDouble();
Value = arguments[2].ToDouble();
}
public CaptorValue(double min, double max, double value)
{
Min = min;
Max = max;
Value = value;
}
public string ToArgument()
{
return $"{Min}{ValueSeparator}{Max}{ValueSeparator}{Value}";
}
public override string ToString()
{
return ToArgument();
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public class CaptorsRequest : CommandSerializable<CaptorsRequest>
{
public override Command Command => Command.Captors;
public override CaptorsRequest Deserialize(string[] arguments)
{
return this;
}
public override string[] Serialize()
{
return Array.Empty<string>();
}
}
}

View File

@@ -0,0 +1,61 @@
using PlantBox.Shared.Extensions;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public class CaptorsResponse : CommandSerializable<CaptorsResponse>
{
public override Command Command => Command.Captors;
public double Humidity { get; set; }
public double Luminosity { get; set; }
public double Temperature { get; set; }
public double Tank { get; set; }
public CaptorsResponse()
{
}
public CaptorsResponse(double humidity, double luminosity, double temperature, double tank)
{
Humidity = humidity;
Luminosity = luminosity;
Temperature = temperature;
Tank = tank;
}
public override CaptorsResponse Deserialize(string[] arguments)
{
if (arguments == null)
{
throw new ArgumentNullException(nameof(arguments));
}
if (arguments.Length < 4)
{
throw new ArgumentException($"Excepted 4 arguments, got {arguments.Length}");
}
Humidity = arguments[0].ToDouble();
Luminosity = arguments[1].ToDouble();
Temperature = arguments[2].ToDouble();
Tank = arguments[3].ToDouble();
return this;
}
public override string[] Serialize()
{
return new[]
{
Humidity.ToArgument(),
Luminosity.ToArgument(),
Temperature.ToArgument(),
Tank.ToArgument()
};
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
/// <summary>
/// A class serializable to a <see cref="CommandPacket"/>
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class CommandSerializable<T> where T : new()
{
/// <summary>
/// Default <see cref="Communication.Command"/>
/// </summary>
public abstract Command Command { get; }
/// <summary>
/// Serialize all the values in a string <see cref="Array"/>,
/// should contains everything to deserialize
/// </summary>
/// <returns>Valid arguments for a <see cref="CommandPacket"/></returns>
public abstract string[] Serialize();
/// <summary>
/// Create an instance of the class from a string <see cref="Array"/>
/// </summary>
/// <param name="arguments">Arguments from the <see cref="CommandSerializable{T}.Serialize"/> method</param>
/// <returns><see cref="T"/> initilized accordingly</returns>
public abstract T Deserialize(string[] arguments);
/// <summary>
/// Convert a <see cref="CommandSerializable{T}"/> to a <see cref="CommandPacket"/>, using <see cref="Command"/>
/// </summary>
/// <param name="id">A valid id</param>
/// <returns></returns>
public CommandPacket ToCommandPacket(ulong id)
{
return new CommandPacket(Command, id, Serialize());
}
/// <summary>
/// Convert a <see cref="CommandSerializable{T}"/> to a <see cref="CommandPacket"/>
/// </summary>
/// <param name="command">Command type</param>
/// <param name="id">A valid id</param>
/// <returns></returns>
public CommandPacket ToCommandPacket(Command command, ulong id)
{
return new CommandPacket(command, id, Serialize());
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public enum HistoricInterval
{
Monthly,
Weekly,
Daily,
Hourly,
Minutely
}
}

View File

@@ -0,0 +1,52 @@
using PlantBox.Shared.Extensions;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public class HistoricRequest : CommandSerializable<HistoricRequest>
{
public override Command Command => Command.Historic;
public HistoricInterval Interval { get; set; }
public int Number { get; set; }
public HistoricRequest()
{
}
public HistoricRequest(HistoricInterval interval, int number)
{
Interval = interval;
Number = number;
}
public override HistoricRequest Deserialize(string[] arguments)
{
if (arguments == null)
{
throw new ArgumentNullException(nameof(arguments));
}
if (arguments.Length < 2)
{
throw new ArgumentException($"Excepted 2 arguments, got {arguments.Length}");
}
Interval = arguments[0].ToEnumValue<HistoricInterval>();
Number = arguments[1].ToInt();
return this;
}
public override string[] Serialize()
{
return new[]
{
Interval.ToString(),
Number.ToArgument()
};
}
}
}

View File

@@ -0,0 +1,73 @@
using PlantBox.Shared.Extensions;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public class HistoricResponse : CommandSerializable<HistoricResponse>
{
public const char ValueSeparator = '/';
public override Command Command => Command.Historic;
public TimeSpan Time { get; set; }
public double[] Humidities { get; set; }
public double[] Luminosities { get; set; }
public double[] Temperatures { get; set; }
public HistoricResponse()
{
}
public HistoricResponse(TimeSpan time, double[] humidities, double[] luminosities, double[] temperatures)
{
Time = time;
Humidities = humidities;
Luminosities = luminosities;
Temperatures = temperatures;
}
public override HistoricResponse Deserialize(string[] arguments)
{
double[] GetArray(string argument)
{
return argument.Split(ValueSeparator).Select(x => x.ToDouble()).ToArray();
}
if (arguments == null)
{
throw new ArgumentNullException(nameof(arguments));
}
if (arguments.Length < 4)
{
throw new ArgumentException($"Excepted 4 arguments, got {arguments.Length}");
}
Time = TimeSpan.FromSeconds(arguments[0].ToDouble());
Humidities = GetArray(arguments[1]);
Luminosities = GetArray(arguments[2]);
Temperatures = GetArray(arguments[3]);
return this;
}
public override string[] Serialize()
{
string Join(double[] values)
{
return string.Join(ValueSeparator.ToString(), values.Select(x => x.ToArgument()));
}
return new[]
{
Time.TotalSeconds.ToArgument(),
Join(Humidities),
Join(Luminosities),
Join(Temperatures)
};
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public class InfoRequest : CommandSerializable<InfoRequest>
{
public override Command Command => Command.Info;
public override InfoRequest Deserialize(string[] arguments)
{
return this;
}
public override string[] Serialize()
{
return Array.Empty<string>();
}
}
}

View File

@@ -0,0 +1,76 @@
using PlantBox.Shared.Extensions;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public class InfoResponse : CommandSerializable<InfoResponse>
{
public override Command Command => Command.Info;
public string Name { get; set; }
public PlantType Type { get; set; }
public PlantState State { get; set; }
public double HumidityMin { get; set; }
public double HumidityMax { get; set; }
public double LuminosityMin { get; set; }
public double LuminosityMax { get; set; }
public double TemperatureMin { get; set; }
public double TemperatureMax { get; set; }
public InfoResponse()
{
}
public InfoResponse(string name, PlantType type, PlantState state, double humidityMin, double humidityMax, double luminosityMin, double luminosityMax, double temperatureMin, double temperatureMax)
{
Name = name;
Type = type;
State = state;
HumidityMin = humidityMin;
HumidityMax = humidityMax;
LuminosityMin = luminosityMin;
LuminosityMax = luminosityMax;
TemperatureMin = temperatureMin;
TemperatureMax = temperatureMax;
}
public override InfoResponse Deserialize(string[] arguments)
{
if (arguments == null)
{
throw new ArgumentNullException(nameof(arguments));
}
if (arguments.Length < 6)
{
throw new ArgumentException($"Excepted 6 arguments, got {arguments.Length}");
}
Name = arguments[0];
Type = arguments[1].ToEnumValue<PlantType>();
State = arguments[2].ToEnumValue<PlantState>();
(HumidityMin, HumidityMax) = arguments[3].Split('/').Select(x => x.ToDouble()).ToArray().ToTuple();
(LuminosityMin, LuminosityMax) = arguments[4].Split('/').Select(x => x.ToDouble()).ToArray().ToTuple();
(TemperatureMin, TemperatureMax) = arguments[5].Split('/').Select(x => x.ToDouble()).ToArray().ToTuple();
return this;
}
public override string[] Serialize()
{
return new[]
{
Name,
Type.ToString(),
State.ToString(),
$"{HumidityMin}/{HumidityMax}",
$"{LuminosityMin}/{LuminosityMax}",
$"{TemperatureMin}/{TemperatureMax}"
};
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public class PingCommand : CommandSerializable<PingCommand>
{
public override Command Command => Command.Ping;
public string Message { get; set; }
public PingCommand()
{
}
public PingCommand(string message)
{
Message = message;
}
public override PingCommand Deserialize(string[] arguments)
{
if (arguments == null)
{
throw new ArgumentNullException(nameof(arguments));
}
if (arguments.Length < 1)
{
throw new ArgumentException($"Excepted at least 1 argument, got {arguments.Length}");
}
Message = arguments[0];
return this;
}
public override string[] Serialize()
{
return new[]
{
Message
};
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public enum PlantState
{
Default,
Bad,
Warning,
Good
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace PlantBox.Shared.Communication.Commands
{
public enum PlantType
{
Default,
Bamboo,
Bonsai,
Cactus,
Ficus
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
namespace PlantBox.Shared.Communication
{
/// <summary>
/// Contains all informations used to communicate
/// </summary>
public static class Connection
{
// UDP
/// <summary>
/// Port used by broadcast discovery
/// </summary>
public const int UDP_PORT = 1401;
/// <summary>
/// Request sent for a discovery
/// </summary>
public static readonly byte[] UDP_REQUEST = { 102, 210, 48, 255 };
/// <summary>
/// Reply sent for a discovery
/// </summary>
public static readonly byte[] UDP_REPLY = { 102, 210, 48, 0 };
// TCP
/// <summary>
/// Port used by the broker to accept servers
/// </summary>
public const int TCP_SERVER_PORT = 1402;
/// <summary>
/// Port used by the broker to accept clients
/// </summary>
public const int TCP_CLIENT_PORT = 1403;
/// <summary>
/// Bytes to read at a time in a <see cref="NetworkStream"/>
/// </summary>
public const int BUFFER_SIZE = 4096;
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace PlantBox.Shared.Extensions
{
public static class CommandSerializeExtensions
{
// Array extensions
public static (T first, T second) ToTuple<T>(this T[] array)
{
return (array[0], array[1]);
}
// String conversion
public static T ToEnumValue<T>(this string argument)
{
return (T)Enum.Parse(typeof(T), argument, true);
}
public static int ToInt(this string argument)
{
return int.Parse(argument, CultureInfo.InvariantCulture);
}
public static double ToDouble(this string argument)
{
return double.Parse(argument, CultureInfo.InvariantCulture);
}
// Double conversion
public static string ToArgument(this double argument)
{
return argument.ToString(CultureInfo.InvariantCulture);
}
// Int conversion
public static string ToArgument(this int argument)
{
return argument.ToString(CultureInfo.InvariantCulture);
}
}
}

View File

@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28307.421
# Visual Studio Version 16
VisualStudioVersion = 16.0.28803.202
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlantBox.Shared", "PlantBox.Shared\PlantBox.Shared.csproj", "{9E73B18F-8D48-43BC-974D-D1697E1085A1}"
EndProject
@@ -296,9 +296,9 @@ Global
{33FE30F1-5344-4565-8456-24322CF17093}.Debug|x64.ActiveCfg = Debug|x64
{33FE30F1-5344-4565-8456-24322CF17093}.Debug|x64.Build.0 = Debug|x64
{33FE30F1-5344-4565-8456-24322CF17093}.Debug|x64.Deploy.0 = Debug|x64
{33FE30F1-5344-4565-8456-24322CF17093}.Debug|x86.ActiveCfg = Debug|x86
{33FE30F1-5344-4565-8456-24322CF17093}.Debug|x86.Build.0 = Debug|x86
{33FE30F1-5344-4565-8456-24322CF17093}.Debug|x86.Deploy.0 = Debug|x86
{33FE30F1-5344-4565-8456-24322CF17093}.Debug|x86.ActiveCfg = Debug|x64
{33FE30F1-5344-4565-8456-24322CF17093}.Debug|x86.Build.0 = Debug|x64
{33FE30F1-5344-4565-8456-24322CF17093}.Debug|x86.Deploy.0 = Debug|x64
{33FE30F1-5344-4565-8456-24322CF17093}.Release|Any CPU.ActiveCfg = Release|x86
{33FE30F1-5344-4565-8456-24322CF17093}.Release|ARM.ActiveCfg = Release|ARM
{33FE30F1-5344-4565-8456-24322CF17093}.Release|ARM.Build.0 = Release|ARM