Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Wood committed May 6, 2020
1 parent 9e0de58 commit 319b06c
Show file tree
Hide file tree
Showing 102 changed files with 9,323 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,5 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

src/lib
164 changes: 163 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,164 @@
# Ukor
A fake Roku Streaming Stick device that can be used as gateway for enabling home automation
A fake Roku Streaming Stick device that can be used as gateway to enable home automation.

## What is it? Why was it written?

I have a Logitech Harmony remote at home and it's connected to a few different devices: Fire TV Cubes, Chromecasts, Rokus, amplifiers, light bulbs, etc. However, I ran into a use case where as part of an activity, I wanted to do something slightly unusual (change the behaviour of my DNS server).

Unfortunately, Harmony doesn't allow you to do such things; you can work with a very specific range of devices, but not beyond those.

The Harmony system can, however, speak to Roku devices and uses HTTP requests to do so.

I decided to see whether I could build a *fake* Roku Streaming Stick+ - i.e. something that imitates the device's simple XML-based HTTP API. It would receive instructions from the Harmony Hub, but rather doing what was requested (e.g. launch a channel or fast-forward), it would do something completely different that could be defined through configuration - either via a command line task or a C# plugin.

Ukor is so-named because it's Roku spelled backwards. I'm clearly not very imaginative!

## What can it do?

Ukor speaks "SSDP" - a Universal Plug And Play protocol - so it can be discovered on your home network by home automation systems such as Logitech's Harmony Hub.

Once discovered, Ukor has been designed to launch applications in two ways:

* using a sequence of Roku button presses; or
* by launching a channel.

For example, you could get a Harmony activity to send `Fwd, Fwd` (i.e. the fast-forward button twice) and use this sequence to launch an executable.

Alternatively, all your configured Ukor applications can be launched as if they were a channel. So much like you'd ordinarily tell a *real* Roku device to launch the Netflix channel, you could instead tell your fake Roku to launch your "Turn On Intruder Alarm" channel.

The reason both methods are offered is that the Harmony Hub only allows you to launch channels in a "Watch TV" activity, which isn't always ideally suited for every Harmony activity. Just being able to do stuff on a simple button press avoids those limitations. However, the channel-based approach may be easier on other automation platforms.

## What can't it do?

Hopefully it's obvious by now: it's *not* a real Roku. So you can't install apps, watch videos or anything like that!

It's simply a means-to-an-end of opening up more automation possibilities within the home.

When setting up Ukor within your home automation system, you'll probably be asked to confirm that your Roku device is now showing stuff on your television. Obviously, just lie and say "yes" in this scenario. We both know it's never going to actually display anything!

## What are the pre-requisites to run Ukor?

To run Ukor, you'll need the [.NET Core 3.1 runtime](https://dotnet.microsoft.com/download/dotnet-core/3.1).

To develop using Ukor, you'll need Visual Studio or Visual Studio Code.

## How do I configure it?

### Software Configuration

All configuration happens via `appsettings.json` .

Within the file, you'll see an array called `Applications`. This is an individual application:

```json
{
"Id": 1000,
"Name": "Set DNS",
"Action": "CSharp",
"CSharpDetails": {
"AssemblyPath": "C:\\MyPlugin\\Chris.Ukor.dll",
"ClassName": "SetDns"
},
"LaunchKeySequence": [
"Left"
]
}
```

Applications have an integer-based `Id` which are used as channel IDs in the fake Roku. Start numbering these at `1000`. You can specify a `Name` that will be used as a channel name as well as an action that is either `CSharp` or `CommandLine`.

For `CSharp` actions, provide a `CSharpDetails` property as shown above. This contains the file path to a .NET Standard or .NET Core DLL containing your plugin and the name of the class to instantiate. All fields are required. Plugin classes must implement the `ICSharpAction` interface found within the Ukor project and must have a parameterless constructor.

For `CommandLine` actions, specify a `CommandLineDetails` property:

```json
{
"Id": 1002,
"Name": "Launch Notepad",
"Action": "CommandLine",
"CommandLineDetails": {
"Executable": "notepad.exe",
"Arguments": null,
"WorkingFolder": null,
"WaitForExit": false
},
"LaunchKeySequence": [
"Fwd"
]
}
```

`Arguments` and `WorkingFolder` are both optional; `Executable` and `WaitForExit` are required fields.

An application can be launched optionally with a `LaunchKeySequence`. This is an array of button presses made up of the following values:

```
Home
Rev
Fwd
Play
Select
Left
Right
Down
Up
Back
InstantReplay
Info
Backspace
Search
Enter
```

The length of the sequence is specified by `KeySequenceLength` at the root of `appsettings.json`. All `LaunchKeySequence` arrays *must* be the same length as `KeySequenceLength`.

In the above configuration examples, the `KeySequenceLength` is 1 since I'll personally never need anywhere near the 15 options afforded to me by the range of buttons above. If you're a bit more ambitious than me, a two-button sequence will give you [225 potential options](two-button-permutations.txt) and the linked file will allow you to work through the permutations systematically. A Harmony Hub typically executes both button presses within the same second, so there is only a minimal overhead of a longer sequence.

### Firewall configuration

You'll need to ensure that the following ports are opened on the machine hosting Ukor:

* UDP port 1900 - this is used to receive SSDP search requests - e.g. when a Harmony Hub looks for devices to add on the local network;
* TCP port 8060 - this is used for Ukor's HTTP API, just like a real Roku.

## How do I run it?

Just run:

```
dotnet Ukor.dll
```

You might want to run Ukor when the hosting machine starts up using something like [NSSM](https://nssm.cc).

## Example ICSharpAction class

This is the typical structure of a C#-based plugin class:

```csharp
using System.Threading.Tasks;
using Ukor.Configuration;

namespace Chris.Ukor
{
public class SetDns : ICSharpAction
{
public async Task DoActionAsync(Application application)
{
// Do whatever async action you like!
// "application" is the application object from your appsettings.json
// that's been triggered by a button-press sequence or channel launch.
}
}
}
```



## Why a custom version of RSSDP?

Ukor uses a customised version of Yortw's [RSSDP](https://github.com/Yortw/RSSDP) library so that the fake Roku device can be discovered on the local network.

As Ukor emulates a *real* Roku device, I needed more control over the USN and Notification Type values emitted by RSSDP to ensure they were faithful to the values a real Roku device would emit. I also needed to respond to search requests beyond those supported out-of-the-box by RSSDP.

The RSSDP-derived code in this repo is therefore a very much cut-down and tweaked version of the official implementation, but I'm incredibly grateful to Yortw for the effort that's been put into that official implementation.
21 changes: 21 additions & 0 deletions RSSDP_LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015 Troy Willmot

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file added src/Rssdp.NetCore/GlobalSuppressions.cs
Binary file not shown.
19 changes: 19 additions & 0 deletions src/Rssdp.NetCore/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("RSSDP.NetCore")]
[assembly: AssemblyDescription(".NET Core specific implementation for RSSDP library.")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

[assembly: CLSCompliant(false)]
[assembly: NeutralResourcesLanguageAttribute("en-US")]
75 changes: 75 additions & 0 deletions src/Rssdp.NetCore/Rssdp.NetCore.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{AB769ED2-0A3D-41B7-A702-0E5B08DA158E}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Rssdp</RootNamespace>
<AssemblyName>Rssdp</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>
</TargetFrameworkProfile>
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>TRACE;DEBUG;CODE_ANALYSIS;NETSTANDARD;NETSTANDARD1_3;CODE_ANALYSIS</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Debug\Rssdp.xml</DocumentationFile>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\RssdpRuleset.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\lib\netstandard13\</OutputPath>
<DefineConstants>TRACE;CODE_ANALYSIS;CODE_ANALYSIS;NETSTANDARD;NETSTANDARD1_3;CODE_ANALYSIS;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>..\lib\netstandard13\Rssdp.xml</DocumentationFile>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\RssdpRuleset.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<None Include="project.json" />
<!-- A reference to the entire .NET Framework is automatically included -->
</ItemGroup>
<ItemGroup>
<Compile Include="..\Shared\SsdpDeviceLocator.cs">
<Link>SsdpDeviceLocator.cs</Link>
</Compile>
<Compile Include="..\Shared\SystemNetSockets\SocketFactory.cs">
<Link>SocketFactory.cs</Link>
</Compile>
<Compile Include="..\Shared\SystemNetSockets\UdpSocket.cs">
<Link>UdpSocket.cs</Link>
</Compile>
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SsdpDevicePublisher.cs" />
</ItemGroup>
<ItemGroup>
<CodeAnalysisDictionary Include="..\Shared\CodeAnalysisDictionary.xml">
<Link>Properties\CodeAnalysisDictionary.xml</Link>
</CodeAnalysisDictionary>
</ItemGroup>
<Import Project="..\Rssdp.Shared\Rssdp.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
92 changes: 92 additions & 0 deletions src/Rssdp.NetCore/SsdpDevicePublisher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Rssdp.Infrastructure;

namespace Rssdp
{
/// <summary>
/// Allows publishing devices both as notification and responses to search requests.
/// </summary>
/// <remarks>
/// This is the 'server' part of the system. You add your devices to an instance of this class so clients can find them.
/// </remarks>
public class SsdpDevicePublisher : SsdpDevicePublisherBase
{

#region Constructors

/// <summary>
/// Default constructor.
/// </summary>
/// <remarks>
/// <para>Uses the default <see cref="ISsdpCommunicationsServer"/> implementation and network settings for Windows and the SSDP specification.</para>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No way to do this here, and we don't want to dispose it except in the (rare) case of an exception anyway.")]
public SsdpDevicePublisher()
: this(new SsdpCommunicationsServer(new SocketFactory(null)))
{

}

/// <summary>
/// Full constructor.
/// </summary>
/// <remarks>
/// <para>Allows the caller to specify their own <see cref="ISsdpCommunicationsServer"/> implementation for full control over the networking, or for mocking/testing purposes..</para>
/// </remarks>
public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer)
: base(communicationsServer, GetOSName(), GetOSVersion())
{

}

/// <summary>
/// Partial constructor.
/// </summary>
/// <param name="localPort">The local port to use for socket communications, specify 0 to have the system choose it's own.</param>
/// <remarks>
/// <para>Uses the default <see cref="ISsdpCommunicationsServer"/> implementation and network settings for Windows and the SSDP specification, but specifies the local port to use for socket communications. Specify 0 to indicate the system should choose it's own port.</para>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No way to do this here, and we don't want to dispose it except in the (rare) case of an exception anyway.")]
public SsdpDevicePublisher(int localPort)
: this(new SsdpCommunicationsServer(new SocketFactory(null), localPort))
{

}

/// <summary>
/// Partial constructor.
/// </summary>
/// <param name="localPort">The local port to use for socket communications, specify 0 to have the system choose it's own.</param>
/// <param name="multicastTimeToLive">The number of hops a multicast packet can make before it expires. Must be 1 or greater.</param>
/// <remarks>
/// <para>Uses the default <see cref="ISsdpCommunicationsServer"/> implementation and network settings for Windows and the SSDP specification, but specifies the local port to use and multicast time to live setting for socket communications.</para>
/// <para>Specify 0 for the <paramref name="localPort"/> argument to indicate the system should choose it's own port.</para>
/// <para>The <paramref name="multicastTimeToLive"/> is actually a number of 'hops' on the network and not a time based argument.</para>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No way to do this here, and we don't want to dispose it except in the (rare) case of an exception anyway.")]
public SsdpDevicePublisher(int localPort, int multicastTimeToLive)
: this(new SsdpCommunicationsServer(new SocketFactory(null), localPort, multicastTimeToLive))
{
}

#endregion

#region Private Methods

private static string GetOSName()
{
return System.Runtime.InteropServices.RuntimeInformation.OSDescription;
}

private static string GetOSVersion()
{
return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
}

#endregion

}
}
Loading

0 comments on commit 319b06c

Please sign in to comment.