Skip to content

Commit

Permalink
Experimental support Rx (#118)
Browse files Browse the repository at this point in the history
## Proposed change
The change implements the System.Reactive (Rx) style of using streams of changes for automations. 

## Refactorings of where logic is put
In order to make application lifecycle easier, all eventlisteners and state handlers are tied to each app-instance. This makes it way more easy to disable individual apps in runtime plus manage lifecykle in the Rx stuff too. 

## Implement the Rx API
Implement the Rx interface. This is an alternative implementation to @jvert suggestions (thanks alot for this great idéa). This proposal was so different it was easier to implement it in own PR rather than change yours.. Hope that is ok :). 

New BaseClass `NetDaemonRxApp` that will implement the Rx style. We do not want to mix with old style. This is event based and not Async/await. All calls post a message on a Channel and async code takes care of the async stuff. 

Tests will be added. after feedback.
More API features will be added before merge. 

Example of usage:

```c#
// Observable that get state changes 
StateChanges.Subscribe(t=> Log(t.New.State));

// Observable that get all state changes inkluding attributes 
StateAllChanges.Subscribe(t=> Log(t.New.State));

// IEnumerable<EntityState>
var allLights = States.Select(n => n.EntityId.StartsWith("light."));
// Gets single state
var state = State("light.my_light")?.State;

// No async, handled in background
CallService("light", "turn_on", new {entity_id = "light.my_light"});

// Entity now not fluent
// Action on single entity
Entity("light.my_light").TurnOn();
Entity("light.my_light").Toggle();
// Action on multiple entities
Entities("light.my_light", "light.my_light").Toggle();
// Or lambda
Entities(n => n.EntityId.StartsWith("light.").Toggle();

// Merging observables <3
Entities(
    "binary_sensor.tomas_rum_pir",
    "binary_sensor.vardagsrum_pir")
    .StateChanges
    .Subscribe(e =>
    {
        Log("{entity}: {state}({oldstate}, {lastchanged})", e.New.EntityId, e.New.State, e.Old.State, e.New.LastChanged);
    });

// Merging observables all changes including attributes <3
Entities(
    "binary_sensor.tomas_rum_pir",
    "binary_sensor.vardagsrum_pir")
    .StateAllChanges
    .Subscribe(e =>
    {
        Log("{entity}: {state}({oldstate}, {lastchanged})", e.New.EntityId, e.New.State, e.Old.State, e.New.LastChanged);
    });

// Set state
SetState("sensor.thesensor", "on", new {attributex="cool"});
// Set state selecting with Entity Selector
Entity("sensor.x", "sensor.y").SetState("on");

// Merging of entity results

// Schedulers
RunEvery(TimeSpan.FromMinutes(1)).Subscribe(....);

RunDaily("12:00:00").Subscribe(...);

// Events
EventChanges
     .Subscribe(f =>
      {
            Log("event: {domain}.{event} - {data}", f?.Domain??"none", f.Event, f?.Data);
       });


```
  • Loading branch information
helto4real committed May 18, 2020
1 parent ad40a2d commit 6aab278
Show file tree
Hide file tree
Showing 58 changed files with 4,614 additions and 2,184 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ TestResults
lcov.info
codecover
**.DS_Store
.generated
.generated
scripts/version.txt
packages/
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ENV \
DOTNET_CLI_TELEMETRY_OPTOUT=true \
HASS_RUN_PROJECT_FOLDER=/usr/src/Service \
HASS_HOST=localhost \
HASSCLIENT_MSGLOGLEVEL=Default \
HASS_PORT=8123 \
HASS_TOKEN=NOT_SET \
HASS_DAEMONAPPFOLDER=/data
Expand Down
44 changes: 44 additions & 0 deletions exampleapps/apps/Extensions/AppExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using JoySoftware.HomeAssistant.NetDaemon.Common.Reactive;
using Helto4real.Powertools;
public static class DaemonAppExtensions
{

/// <summary>
/// Takes a snapshot of given entity id of camera and sends to private discord server
/// </summary>
/// <param name="app">NetDaemonApp to extend</param>
/// <param name="camera">Unique id of the camera</param>
public static void CameraTakeSnapshotAndNotify(this NetDaemonRxApp app, string camera)
{
var imagePath = app.CameraSnapshot(camera);

app.NotifyImage(camera, imagePath);
}

public static void Notify(this NetDaemonRxApp app, string message)
{
app.CallService("notify", "hass_discord", new
{
message = message,
target = "511278310584746008"
});
}

public static void NotifyImage(this NetDaemonRxApp app, string message, string imagePath)
{
var dict = new Dictionary<string, IEnumerable<string>>
{
["images"] = new List<string> { imagePath }
};

app.CallService("notify", "hass_discord", new
{
data = dict,
message = message,
target = "511278310584746008"
});
}
}
58 changes: 58 additions & 0 deletions exampleapps/apps/Extensions/ServiceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@

using System;
using System.Threading.Tasks;
using JoySoftware.HomeAssistant.NetDaemon.Common;

// public static class ServiceCallExtensions
// {
// public static Task ToggleAsync(this NetDaemonApp app, string entityId, params (string name, object val)[] attributeNameValuePair)
// {
// // Get the domain if supported, else domain is homeassistant
// string domain = GetDomainFromEntity(entityId);
// // Use it if it is supported else use default "homeassistant" domain

// // Use expando object as all other methods
// dynamic attributes = attributeNameValuePair.ToDynamic();
// // and add the entity id dynamically
// attributes.entity_id = entityId;

// return app.CallService(domain, "toggle", attributes, false);
// }

// public static Task TurnOffAsync(this NetDaemonApp app, string entityId, params (string name, object val)[] attributeNameValuePair)
// {
// // Get the domain if supported, else domain is homeassistant
// string domain = GetDomainFromEntity(entityId);
// // Use it if it is supported else use default "homeassistant" domain

// // Use expando object as all other methods
// dynamic attributes = attributeNameValuePair.ToDynamic();
// // and add the entity id dynamically
// attributes.entity_id = entityId;

// return app.CallService(domain, "turn_off", attributes, false);
// }

// public static Task TurnOnAsync(this NetDaemonApp app, string entityId, params (string name, object val)[] attributeNameValuePair)
// {
// // Use default domain "homeassistant" if supported is missing
// string domain = GetDomainFromEntity(entityId);
// // Use it if it is supported else use default "homeassistant" domain

// // Convert the value pairs to dynamic type
// dynamic attributes = attributeNameValuePair.ToDynamic();
// // and add the entity id dynamically
// attributes.entity_id = entityId;

// return app.CallService(domain, "turn_on", attributes, false);
// }

// private static string GetDomainFromEntity(string entity)
// {
// var entityParts = entity.Split('.');
// if (entityParts.Length != 2)
// throw new ApplicationException($"entity_id is mal formatted {entity}");

// return entityParts[0];
// }
// }
65 changes: 65 additions & 0 deletions exampleapps/apps/Powertools/Powertools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using JoySoftware.HomeAssistant.NetDaemon.Common.Reactive;

// Use unique namespaces for your apps if you going to share with others to avoid
// conflicting names
namespace Helto4real.Powertools
{
public static class Powertools
{
/// <summary>
/// Takes a snapshot of given entity id of camera and sends to private discord server
/// </summary>
/// <param name="app">NetDaemonApp to extend</param>
/// <param name="camera">Unique id of the camera</param>
/// <returns>The path to the snapshot</returns>
public static string CameraSnapshot(this NetDaemonRxApp app, string camera)
{
var resultingFilename = $"/config/www/motion/{camera}_latest.jpg";
app.CallService("camera", "snapshot", new
{
entity_id = camera,
filename = resultingFilename
});

return resultingFilename;
}

/// <summary>
/// Takes a snapshot of given entity id of camera and sends to private discord server
/// </summary>
/// <param name="app">NetDaemonApp to extend</param>
/// <param name="camera">Unique id of the camera</param>
public static void CameraSnapshot(this NetDaemonRxApp app, string camera, string snapshotPath)
{
app.CallService("camera", "snapshot", new
{
entity_id = camera,
filename = snapshotPath
});
}

/// <summary>
/// Prints the contents from a IDictionary to a string
/// </summary>
/// <param name="app">NetDaemonApp to extend</param>
/// <param name="dict">The dict to print from, typically from dynamic result</param>
/// <returns></returns>
public static string PrettyPrintDictData(this NetDaemonRxApp app, IDictionary<string, object>? dict)
{

if (dict == null)
return string.Empty;

var builder = new StringBuilder(100);
foreach (var key in dict.Keys)
{
builder.AppendLine($"{key}:{dict[key]}");
}
return builder.ToString();
}
}
}

25 changes: 8 additions & 17 deletions exampleapps/apps/test1.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
using System.Threading.Tasks;
using JoySoftware.HomeAssistant.NetDaemon.Common;
using JoySoftware.HomeAssistant.NetDaemon.Common.Reactive;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Reactive.Linq;
using System.Runtime.Serialization;
// using Netdaemon.Generated.Extensions;
public class GlobalApp : NetDaemonApp
{
// private ISchedulerResult _schedulerResult;
private int numberOfRuns = 0;

public string? SharedThing { get; set; }
public override async Task InitializeAsync()
IDisposable task;
//public string? SharedThing { get; set; }
public override Task InitializeAsync()
{
SharedThing = "Hello world";
Log("Logging from global app");
LogError("OMG SOMETING IS WRONG {error}", "The error!");

Entities(p =>
{
// await Task.Delay(5000);
Thread.Sleep(10);
return false;
}).WhenStateChange("on", "off")
.Call((a, b, c) => { Log("Logging from global app"); return Task.CompletedTask; })
.Execute();
Event("TEST_EVENT").Call(async (ev, data) => { Log("EVENT!"); }).Execute();

Log("AfterExecute");
// Entity("light.my_light").TurnOn();
return Task.CompletedTask;
}
}

Expand Down
30 changes: 28 additions & 2 deletions exampleapps/apps/test2.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.Threading.Tasks;
using JoySoftware.HomeAssistant.NetDaemon.Common;
using JoySoftware.HomeAssistant.NetDaemon.Common.Reactive;
using System.Linq;
using System;
using System.Reactive.Linq;
using System.Collections.Generic;
// using Netdaemon.Generated.Extensions;
public class BatteryManager : NetDaemonApp
public class BatteryManager : NetDaemonRxApp
{
// private ISchedulerResult _schedulerResult;
private int numberOfRuns = 0;
Expand All @@ -14,6 +15,31 @@ public override async Task InitializeAsync()
{
Log("Hello");
Log("Hello {name}", "Tomas");
// RunEvery(TimeSpan.FromSeconds(5), () => Log("Hello world!"));
// RunDaily("13:00:00", () => Log("Hello world!"));
// RunIn(TimeSpan.FromSeconds(5), () => Entity("light.tomas_rum").TurnOn());
// Entity("light.tomas_rum")
// .StateChanges
// .Subscribe(s => Log("Chanche {entity}", s.New.State));

// StateChanges
// .Where(e => e.New.EntityId.StartsWith("light."))
// .Subscribe(e =>
// {
// Log("Hello!");
// });
// EventChanges
// // .Where(e => e.Domain == "scene" && e.Event == "turn_on")
// .Subscribe(e =>
// {
// Log("Hello!");
// },
// err => LogError(err, "Ohh nooo!"),
// () => Log("Ending event"));



// Event("TEST_EVENT").Call(async (ev, data) => { Log("EVENT2!"); }).Execute();

// Scheduler.RunEvery(5000, () => { var x = 0; var z = 4 / x; return Task.CompletedTask; });
// Entity("sun.sun").WhenStateChange(allChanges: true).Call((entityid, to, from) => throw new Exception("Test")).Execute();
Expand Down
8 changes: 4 additions & 4 deletions exampleapps/apps/test2.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
my_app:
class: BatteryManager
# HelloWorldSecret: !secret test_secret
dependencies:
- global_app
class: BatteryManager
# HelloWorldSecret: !secret test_secret
# dependencies:
# - global_app
45 changes: 29 additions & 16 deletions scripts/packandcopy.ps1
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
Invoke-Expression "del D:\GIT\netdaemon\src\App\NetDaemon.App\bin\Debug\*.nupkg"
Invoke-Expression "del D:\GIT\netdaemon\src\Daemon\NetDaemon.Daemon\bin\Debug\*.nupkg"
Invoke-Expression "del D:\GIT\netdaemon\src\DaemonRunner\DaemonRunner\bin\Debug\*.nupkg"
#
# This script is building the nuget packages for you to import and test in a dev environment
# for building apps. Run once to create the version file. Then set the version you want to use
# run script and the packages is copied to ./packages folder.
# In your app dev environment,
# - change version in csproj file
# - Do a "dotnet restore -s [path_to_package_folder]"
#
# RUN THIS FILE FROM PROJECT ROOT!
#

Invoke-Expression "dotnet pack ..\src\"
# Create the version file if not exists
if ((Test-Path ./scripts/version.txt) -eq $False) {
New-Item -Path ./scripts -Name "version.txt" -ItemType "file" -Value "0.0.1-beta"
}

Invoke-Expression "del D:\GIT\daemonapp\packs\JoySoftware.NetDaemon.App\*.nupkg"
Invoke-Expression "cp D:\GIT\netdaemon\src\App\NetDaemon.App\bin\Debug\*.nupkg D:\GIT\daemonapp\packs\JoySoftware.NetDaemon.App\"
Invoke-Expression "del D:\GIT\daemontest\cs\package\JoySoftware.NetDaemon.App\*.nupkg"
Invoke-Expression "cp D:\GIT\netdaemon\src\App\NetDaemon.App\bin\Debug\*.nupkg D:\GIT\daemontest\cs\package\JoySoftware.NetDaemon.App\"
if ((Test-Path ./packages) -eq $False) {
New-Item . -Name "packages" -ItemType "directory"
}
# Remove all current nuget packages
Get-ChildItem ./src -file -recurse "*.nupkg" | Remove-Item

Invoke-Expression "del D:\GIT\daemonapp\packs\JoySoftware.NetDaemon.Daemon\*.nupkg"
Invoke-Expression "cp D:\GIT\netdaemon\src\Daemon\NetDaemon.Daemon\bin\Debug\*.nupkg D:\GIT\daemonapp\packs\JoySoftware.NetDaemon.Daemon\"
Invoke-Expression "del D:\GIT\daemontest\cs\package\JoySoftware.NetDaemon.Daemon\*.nupkg"
Invoke-Expression "cp D:\GIT\netdaemon\src\Daemon\NetDaemon.Daemon\bin\Debug\*.nupkg D:\GIT\daemontest\cs\package\JoySoftware.NetDaemon.Daemon\"
# Get version to be used
$version = Get-Content ./scripts/version.txt

# Pack
dotnet pack -p:PackageVersion=$version

# Copy the two app packages
Get-ChildItem ./src/App -file -recurse "*.nupkg" | Copy-Item -Destination "./packages"
Get-ChildItem ./src/DaemonRunner -file -recurse "*.nupkg" | Copy-Item -Destination "./packages"
Get-ChildItem ./src/Daemon -file -recurse "*.nupkg" | Copy-Item -Destination "./packages"

Invoke-Expression "del D:\GIT\daemonapp\packs\JoySoftware.NetDaemon.DaemonRunner\*.nupkg"
Invoke-Expression "cp D:\GIT\netdaemon\src\DaemonRunner\DaemonRunner\bin\Debug\*.nupkg D:\GIT\daemonapp\packs\JoySoftware.NetDaemon.DaemonRunner\"
Invoke-Expression "del D:\GIT\daemontest\cs\package\JoySoftware.NetDaemon.DaemonRunner\*.nupkg"
Invoke-Expression "cp D:\GIT\netdaemon\src\DaemonRunner\DaemonRunner\bin\Debug\*.nupkg D:\GIT\daemontest\cs\package\JoySoftware.NetDaemon.DaemonRunner\"
Loading

0 comments on commit 6aab278

Please sign in to comment.