electronics/TPLinkKasaClient/CryptoUtils.cs

current previous
1,81 0,0
+
// 
+
// Copyright (c) 2020 smdn <smdn@smdn.jp>
+
// 
+
// 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.
+

         
+
using System;
+
using System.Buffers;
+
using System.Buffers.Binary;
+
using System.Text;
+

         
+
namespace Smdn.TPLinkKasaClient {
+
  /// <summary>
+
  /// C# implementation of TP-Link Kasa API crypto methods, ported from https://github.com/plasticrake/tplink-smarthome-crypto
+
  /// </summary>
+
  public static class CryptoUtils {
+
    private const int defaultKey = 0xAB;
+

         
+
    public static Memory<byte> Encrypt(
+
      string input,
+
      byte firstKey = defaultKey
+
    )
+
    {
+
      var data = Encoding.UTF8.GetBytes(input ?? throw new ArgumentNullException(nameof(input)));
+
      var memory = (Memory<byte>)new byte[4 + data.Length];
+

         
+
      BinaryPrimitives.WriteInt32BigEndian(memory.Span, data.Length);
+

         
+
      var span = memory.Span.Slice(4);
+
      var key = firstKey;
+

         
+
      for (var i = 0; i < data.Length; i++) {
+
        key = span[i] = (byte)(data[i] ^ key);
+
      }
+

         
+
      return memory;
+
    }
+

         
+
    public static Memory<byte> Decrypt(
+
      ReadOnlySequence<byte> input,
+
      byte firstKey = defaultKey
+
    )
+
    {
+
      var reader = new SequenceReader<byte>(input);
+

         
+
      if (!reader.TryReadBigEndian(out int length))
+
        throw new System.IO.InvalidDataException("input too short (expects at least 4 bytes of header)");
+

         
+
      if (reader.Remaining < length)
+
        throw new System.IO.InvalidDataException($"input too short (expects at least {length} bytes of data body, but is {reader.Remaining} bytes)");
+

         
+
      var decrypted = (Memory<byte>)new byte[length];
+
      var span = decrypted.Span;
+
      var key = firstKey;
+

         
+
      for (var i = 0; i < length; i++) {
+
        reader.TryRead(out var b);
+

         
+
        span[i] = (byte)(b ^ key);
+
        key = b;
+
      }
+

         
+
      return decrypted;
+
    }
+
  }
+
}

electronics/TPLinkKasaClient/SimpleClient.cs

current previous
1,192 0,0
+
// 
+
// Copyright (c) 2020 smdn <smdn@smdn.jp>
+
// 
+
// 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.
+

         
+
using System;
+
using System.Buffers;
+
using System.Collections.Generic;
+
using System.Net;
+
using System.Net.Sockets;
+
using System.Linq;
+
using System.Text.Json;
+
using System.Threading;
+
using System.Threading.Tasks;
+

         
+
namespace Smdn.TPLinkKasaClient {
+
  /// <summary>
+
  /// C# implementation of TP-Link Kasa API client, ported from https://github.com/plasticrake/tplink-smarthome-api
+
  /// </summary>
+
  public class SimpleClient {
+
    public const int DefaultPort = 9999;
+

         
+
    public string Host { get; }
+
    public int Port { get; }
+

         
+
    public SimpleClient(
+
      string host,
+
      int port = DefaultPort
+
    )
+
    {
+
      this.Host = string.IsNullOrEmpty(host)
+
        ? throw new ArgumentException($"'{nameof(host)}' must be non-empty string", nameof(host))
+
        : host;
+
      this.Port = port < IPEndPoint.MinPort || IPEndPoint.MaxPort < port
+
        ? throw new ArgumentOutOfRangeException(nameof(port), port, $"must be in range of {IPEndPoint.MinPort}~{IPEndPoint.MaxPort}")
+
        : port;
+
    }
+

         
+
    public Task<JsonDocument> SendAsync(
+
      string module,
+
      string method
+
    )
+
      => SendAsync(
+
        module,
+
        method,
+
        methodParameters: (IEnumerable<(string, object)>)null
+
      );
+

         
+
    public Task<JsonDocument> SendAsync(
+
      string module,
+
      string method,
+
      IEnumerable<(string name, object value)> methodParameters
+
    )
+
      => SendCommandAsync(
+
        module,
+
        method,
+
        methodParameters?.Select(ConvertToJsonFormat)
+
      );
+

         
+
    public Task<JsonDocument> SendAsync(
+
      string module,
+
      string method,
+
      IEnumerable<KeyValuePair<string, object>> methodParameters
+
    )
+
      => SendCommandAsync(
+
        module,
+
        method,
+
        methodParameters?.Select(pair => ConvertToJsonFormat((pair.Key, pair.Value)))
+
      );
+

         
+
    private static (string, string) ConvertToJsonFormat(
+
      (string name, object value) prop
+
    )
+
      => (
+
        prop.name,
+
        prop.value switch {
+
          bool b => b ? "true" : "false",
+
          string s => string.Concat("\"", s.Replace("\"", "\\\""), "\""),
+
          //int i => i.ToString(),
+
          //double d => d.ToString(),
+
          null => "null",
+
          _ => Convert.ToString(prop.value),
+
        }
+
      );
+

         
+
    private async Task<JsonDocument> SendCommandAsync(
+
      string module,
+
      string method,
+
      IEnumerable<(string name, string value)> nullableMethodParameters)
+
    {
+
      string ConstructJsonDocument()
+
        => @$"{{
+
  ""{module}"":{{
+
    ""{method}"":{{
+
  {string.Join(",\n", nullableMethodParameters?.Select(p => $"\"{p.name}\":{p.value}") ?? Enumerable.Empty<string>())}
+
    }}
+
  }}
+
}}";
+

         
+
      using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) {
+
        /*
+
        * connect
+
        */
+
        await socket.ConnectAsync(Host, Port).ConfigureAwait(false);
+

         
+
        /*
+
        * send
+
        */
+
        await socket.SendAsync(
+
          CryptoUtils.Encrypt(ConstructJsonDocument()),
+
          default(SocketFlags),
+
          default(CancellationToken)
+
        ).ConfigureAwait(false);
+

         
+
        /*
+
        * receive
+
        */
+
        SequenceSegment receivedSequenceHead = null;
+
        SequenceSegment receivedSequenceTail = null;
+

         
+
        for (;;) {
+
          const int bufferSize = 0x100;
+
          var buffer = (Memory<byte>)new byte[bufferSize];
+
          var len = await socket.ReceiveAsync(
+
            buffer,
+
            default(SocketFlags),
+
            default(CancellationToken)
+
          ).ConfigureAwait(false);
+

         
+
          if (len <= 0)
+
            break;
+

         
+
          if (receivedSequenceHead == null)
+
            receivedSequenceTail = receivedSequenceHead = new SequenceSegment(null, buffer);
+
          else
+
            receivedSequenceTail = new SequenceSegment(receivedSequenceTail, buffer);
+

         
+
          if (len < buffer.Length)
+
            break;
+
        }
+

         
+
        /*
+
        * parse
+
        */
+
        var receivedSequence = new ReadOnlySequence<byte>(
+
          receivedSequenceHead,
+
          0,
+
          receivedSequenceTail,
+
          receivedSequenceTail.Memory.Length
+
        );
+

         
+
        var response = CryptoUtils.Decrypt(receivedSequence);
+

         
+
        return JsonDocument.Parse(
+
          response,
+
          default(JsonDocumentOptions)
+
        );
+
      }
+
    }
+

         
+
    private class SequenceSegment : ReadOnlySequenceSegment<byte> {
+
      public SequenceSegment(SequenceSegment prev, ReadOnlyMemory<byte> memory)
+
      {
+
        Memory = memory;
+

         
+
        if (prev == null) {
+
          RunningIndex = 0;
+
        }
+
        else {
+
          RunningIndex = prev.RunningIndex + prev.Memory.Length;
+
          prev.Next = this;
+
        }
+
      }
+
    }
+
  }
+
}

electronics/TPLinkKasaClient/index.wiki.txt

current previous
1,469 0,0
+
%smdncms%(set-metadata,title,TPLinkKasaClient)
+
%smdncms%(set-metadata,full-title,TPLinkKasaClient (TP-Link KL130/HS105 APIクライアント実装)
+

         
+
TP-Link製スマートホーム製品のAPIにアクセスするための[[クライアント>#client_implementation]]および[[サンプルコード>#examples]]。
+

         
+
KasaスマートLEDランプ[[KL130:https://www.tp-link.com/jp/home-networking/smart-bulb/kl130/]]およびWi-Fiスマートプラグ[[HS105:https://www.tp-link.com/jp/home-networking/smart-plug/hs105/]]の操作や、デバイス情報・現在の状態等の取得ができる。
+

         
+
#remarks
+
本実装は[[TP-Link Smarthome WiFi API:https://github.com/plasticrake/tplink-smarthome-api]]の実装の一部分をC#/.NETに移植したものです。 実機での動作確認はしていますが、APIやプロトコルを独自に解析・調査して実装したものではありません。
+
#remarks-end
+

         
+

         
+

         
+
*動作確認済み製品
+
以下の製品にて、以下の操作が機能することを確認済み。
+

         
+
:[[KL130:https://www.tp-link.com/jp/home-networking/smart-bulb/kl130/]] (firmware version 1.8.11時点)|オンオフ操作、色調・明るさ操作、各種状態取得、デバイス情報取得、電力使用量取得
+
:[[HS105:https://www.tp-link.com/jp/home-networking/smart-plug/hs105/]] (firmware version 1.5.7時点)|オンオフ操作、各種状態取得、デバイス情報取得
+

         
+
実機での動作は未確認ながら[[KL110:https://www.tp-link.com/jp/home-networking/smart-bulb/kl110/]]の操作もできるものと思われます。
+

         
+
#column
+
・KL130
+
&asin(B07GBQN4ND);
+
#column
+
・KL110
+
&asin(B07GC4JR83);
+
#column
+
・HS105
+
&asin(B078HSBNMT);
+
#column-end
+

         
+
*動作確認済み環境
+
以下の環境にて動作確認済み。 要[[.NET Core 3.1 Runtime:https://dotnet.microsoft.com/download/dotnet-core/]]。
+

         
+
-Raspbian 9.11 (Raspberry Pi 3 Model B+)
+
-Ubuntu 18.04
+

         
+
未確認ながらWindows等でも問題なく動作すると思われます。
+

         
+

         
+

         
+
*制限事項・注意事項
+
本クライアントで使用するAPIは、''デバイスと同一ネットワーク内からのみ''使用可能。 そのため、インターネットからの遠隔操作等はできない。
+

         
+
本クライアントで使用するAPIは''正規に公開されているAPIではない''ため、ファームウェアアップデート等により使用できなくなる可能性がある。
+

         
+
他のデバイスとの連携や、ネットワーク外からのアクセス、また正規のAPIは[[KasaのIFTTT service:https://ifttt.com/kasa]]を使用する必要がある。
+

         
+

         
+

         
+
''本クライアントは設定の取得・変更等、製品仕様の範囲内での操作のみを行うものであり、ファームウェアの改変・修正および製品の改造や製品仕様の変更に相当する結果は引き起こさないものの、製品使用上の許諾事項に抵触する可能性は否定できないため、その点ご留意ください。''
+

         
+

         
+

         
+
*参考資料
+

         
+
-[[plasticrake/tplink-smarthome-api:https://github.com/plasticrake/tplink-smarthome-api]]
+
-[[plasticrake/tplink-smarthome-crypto:https://github.com/plasticrake/tplink-smarthome-crypto]]
+
-[[Storming the Kasa? Security analysis of TP-Link Kasa smart home devices devices - Andrew Halterman:https://lib.dr.iastate.edu/cgi/viewcontent.cgi?article=1424&context=creativecomponents]] (PDF)
+

         
+

         
+

         
+
*サンプルコード・実行結果例 [#examples]
+
以下のサンプルコードで使用するクライアント実装``SimpleClient``自体のコードは、[[#client_implementation]]に掲載。
+

         
+
**KL130
+
***ライトのオン/オフ・明るさ・色調の変更
+
ライトのオン/オフのほか、明るさと色調も同時に変更できる。 色調の変更は、色温度(``color_temp``)での指定、もしくは色相(``hue``)と彩度(``saturation``)での指定のどちらかで行う。
+

         
+
また、遷移時間(``transition_period``)を指定すれば、即座にオンにするだけでなく、オフ状態から徐々に明るくしてオンにするような変更もできる。
+

         
+
#code(cs,KL130 transition_light_state){{
+
using System;
+
using System.Text.Json;
+
using System.Threading.Tasks;
+

         
+
using Smdn.TPLinkKasaClient;
+

         
+
class Program
+
{
+
  static async Task Main(string[] args)
+
  {
+
    var client = new SimpleClient("192.168.xxx.xxx");// KL130のアドレスまたはホスト名を指定する
+

         
+
    const string module = "smartlife.iot.smartbulb.lightingservice";
+
    const string method = "transition_light_state";
+

         
+
    var response = await client.SendAsync(
+
      module,
+
      method,
+
      new (string, object) [] {
+
        ("on_off", 1),                // 1でオン・0でオフにする
+
        ("ignore_default", 1),        // 「アプリでオンにした場合」の動作設定を無視して変更する
+
        ("brightness", 100),          // 明るさ0〜100[%]を指定
+
        //("color_temp", 7500),       // 色温度2000〜9500[K]を指定
+
        ("color_temp", 0),            //   色調を色相・彩度で指定する場合は色温度を0にする
+
        ("hue", 280),                 // 色相0〜360[°]を指定する
+
        ("saturation", 60)            // 彩度0〜100[%]を指定する
+
        ("transition_period", 1000),  // 遷移時間[ミリ秒]を指定、0を指定するか省略した場合は即座に反映される
+
      }
+
    );
+

         
+
    // レスポンスを解析する必要がなければ、以下は省略できる
+
    if (!response.RootElement.TryGetProperty(module, out var moduleResponse))
+
      throw new ApplicationException("unexpected response: module element not found");
+

         
+
    if (!moduleResponse.TryGetProperty(method, out var methodResponse))
+
      throw new ApplicationException("unexpected response: method element not found");
+

         
+
    Console.WriteLine(methodResponse.GetRawText());
+
  }
+
}
+
}}
+

         
+
***ライトのオン/オフ・明るさ・色調の状態取得
+

         
+
#code(cs,KL130 get_light_state){{
+
using System;
+
using System.Text.Json;
+
using System.Threading.Tasks;
+

         
+
using Smdn.TPLinkKasaClient;
+

         
+
// ライトの状態を格納する構造体
+
struct LightState {
+
  public int on_off { get; set; }
+
  public string mode { get; set; }
+
  public int hue { get; set; }
+
  public int saturation { get; set; }
+
  public int color_temp { get; set; }
+
  public int brightness { get; set; }
+
  public int err_code { get; set; }
+
}
+

         
+
class Program
+
{
+
  static async Task Main(string[] args)
+
  {
+
    var client = new SimpleClient("192.168.xxx.xxx"); // KL130のアドレスまたはホスト名を指定する
+

         
+
    const string module = "smartlife.iot.smartbulb.lightingservice";
+
    const string method = "get_light_state";
+

         
+
    var response = await client.SendAsync(
+
      module,
+
      method
+
    );
+

         
+
    if (!response.RootElement.TryGetProperty(module, out var moduleResponse))
+
      throw new ApplicationException("unexpected response: module element not found");
+

         
+
    if (!moduleResponse.TryGetProperty(method, out var methodResponse))
+
      throw new ApplicationException("unexpected response: method element not found");
+

         
+
    var lightState = JsonSerializer.Deserialize<LightState>(methodResponse.GetRawText());
+

         
+
    Console.WriteLine($@"state: {(lightState.on_off == 0 ? "off" : "on")}");
+

         
+
    if (lightState.on_off != 0) {
+
      Console.WriteLine($@"brightness: {lightState.brightness}");
+

         
+
      if (lightState.color_temp == 0) {
+
        Console.WriteLine($@"hue: {lightState.hue}");
+
        Console.WriteLine($@"saturation: {lightState.saturation}");
+
      }
+
      else {
+
        Console.WriteLine($@"color temperature: {lightState.color_temp}");
+
      }
+
    }
+
  }
+
}
+
}}
+

         
+
#prompt(実行結果・オフの場合){{
+
state: off
+
}}
+

         
+
#prompt(実行結果・オンの場合・色調が色温度で設定されている場合){{
+
state: on
+
brightness: 55
+
color temperature: 3040
+
}}
+

         
+
#prompt(実行結果・オンの場合・色調が色相・彩度で設定されている場合){{
+
state: on
+
brightness: 87
+
hue: 263
+
saturation: 52
+
}}
+

         
+
***電力使用量の取得
+
特定の年における、月ごとの電力使用量を、ワット時[Wh]単位で取得することができる。
+

         
+
#code(cs,KL130 get_monthstat){{
+
using System;
+
using System.Text.Json;
+
using System.Threading.Tasks;
+

         
+
using Smdn.TPLinkKasaClient;
+

         
+
class Program
+
{
+
  static async Task Main(string[] args)
+
  {
+
    var client = new SimpleClient("192.168.xxx.xxx"); // KL130のアドレスまたはホスト名を指定する
+

         
+
    const string module = "smartlife.iot.common.emeter";
+
    const string method = "get_monthstat";
+

         
+
    var response = await client.SendAsync(
+
      module,
+
      method,
+
      new (string, object) [] {
+
        ("year", 2020), // 2020年の月毎の電力使用量を取得する
+
      }
+
    );
+

         
+
    if (!response.RootElement.TryGetProperty(module, out var moduleResponse))
+
      throw new ApplicationException("unexpected response: module element not found");
+

         
+
    if (!moduleResponse.TryGetProperty(method, out var methodResponse))
+
      throw new ApplicationException("unexpected response: method element not found");
+

         
+
    Console.WriteLine(methodResponse.GetRawText());
+
  }
+
}
+
}}
+

         
+
JSONフォーマットで以下のような内容が得られる。 (見やすさのため整形を施してある)
+

         
+
#code(json,get_monthstatで得られるレスポンス){{
+
{
+
  "month_list": [
+
    {
+
      "year": 2020,
+
      "month": 1,
+
      "energy_wh": 99999
+
    },
+
    {
+
      "year": 2020,
+
      "month": 2,
+
      "energy_wh": 99999
+
    },
+
    {
+
      "year": 2020,
+
      "month": 3,
+
      "energy_wh": 99999
+
    },
+
    {
+
      "year": 2020,
+
      "month": 4,
+
      "energy_wh": 99999
+
    }
+
  ],
+
  "err_code": 0
+
}
+
}}
+

         
+

         
+

         
+

         
+
また、特定の年月における、日ごとの電力使用量を取得することもできる。
+

         
+
#code(cs,KL130 get_daystat){{
+
using System;
+
using System.Text.Json;
+
using System.Threading.Tasks;
+

         
+
using Smdn.TPLinkKasaClient;
+

         
+
class Program
+
{
+
  static async Task Main(string[] args)
+
  {
+
    var client = new SimpleClient("192.168.xxx.xxx"); // KL130のアドレスまたはホスト名を指定する
+

         
+
    const string module = "smartlife.iot.common.emeter";
+
    const string method = "get_daystat";
+

         
+
    var response = await client.SendAsync(
+
      module,
+
      method,
+
      new (string, object) [] {
+
         // 2020年4月の日毎の電力使用量を取得する
+
        ("year", 2020),
+
        ("month", 4),
+
      }
+
    );
+

         
+
    if (!response.RootElement.TryGetProperty(module, out var moduleResponse))
+
      throw new ApplicationException("unexpected response: module element not found");
+

         
+
    if (!moduleResponse.TryGetProperty(method, out var methodResponse))
+
      throw new ApplicationException("unexpected response: method element not found");
+

         
+
    Console.WriteLine(methodResponse.GetRawText());
+
  }
+
}
+
}}
+

         
+

         
+
JSONフォーマットで以下のような内容が得られる。 (見やすさのため整形を施してある) 必ずしも日付順での並びとはならない模様。
+

         
+
#code(json,get_daystatで得られるレスポンス){{
+
{
+
  "day_list": [
+
    {
+
      "year": 2020,
+
      "month": 4,
+
      "day": 8,
+
      "energy_wh": 999
+
    },
+
    {
+
      "year": 2020,
+
      "month": 4,
+
      "day": 9,
+
      "energy_wh": 999
+
    },
+
        :
+
        :
+
      ✂中略
+
        :
+
        :
+
    {
+
      "year": 2020,
+
      "month": 4,
+
      "day": 1,
+
      "energy_wh": 999
+
    },
+
    {
+
      "year": 2020,
+
      "month": 4,
+
      "day": 2,
+
      "energy_wh": 999
+
    },
+
    {
+
      "year": 2020,
+
      "month": 4,
+
      "day": 3,
+
      "energy_wh": 999
+
    },
+
        :
+
        :
+
      ✂中略
+
        :
+
        :
+
  ],
+
  "err_code": 0
+
}
+
}}
+

         
+

         
+

         
+
**HS105
+
***スイッチのオン/オフ
+
#code(cs,HS105 set_relay_state){{
+
using System;
+
using System.Text.Json;
+
using System.Threading.Tasks;
+

         
+
using Smdn.TPLinkKasaClient;
+

         
+
class Program
+
{
+
  static async Task Main(string[] args)
+
  {
+
    var client = new SimpleClient("192.168.xxx.xxx");// HS105のアドレスまたはホスト名を指定する
+

         
+
    const string module = "system";
+
    const string method = "set_relay_state";
+

         
+
    var response = await client.SendAsync(
+
      module,
+
      method,
+
      new (string, object) [] {
+
        ("state", 0) // 0でオフ、1でオンにできる
+
      }
+
    );
+

         
+
    // レスポンスを解析する必要がなければ、以下は省略できる
+
    if (!response.RootElement.TryGetProperty(module, out var moduleResponse))
+
      throw new ApplicationException("unexpected response: module element not found");
+

         
+
    if (!moduleResponse.TryGetProperty(method, out var methodResponse))
+
      throw new ApplicationException("unexpected response: method element not found");
+

         
+
    Console.WriteLine(methodResponse.GetRawText());
+
  }
+
}
+
}}
+

         
+
***スイッチのオン/オフ状態の取得
+
オン/オフ状態のほか、オン状態の場合はオンになってからの経過時間も取得できる。 ``get_sysinfo``は、オン/オフ状態以外にも様々な情報を返す。
+

         
+
#code(cs,HS105 get_sysinfo){{
+
using System;
+
using System.Text.Json;
+
using System.Threading.Tasks;
+

         
+
using Smdn.TPLinkKasaClient;
+

         
+
struct SysInfo {
+
  public int relay_state { get; set; }
+
  public int on_time { get; set; }
+
}
+

         
+
class Program
+
{
+
  static async Task Main(string[] args)
+
  {
+
    var client = new SimpleClient("192.168.xxx.xxx");// HS105のアドレスまたはホスト名を指定する
+

         
+
    const string module = "system";
+
    const string method = "get_sysinfo";
+

         
+
    var response = await client.SendAsync(
+
      module,
+
      method
+
    );
+

         
+
    if (!response.RootElement.TryGetProperty(module, out var moduleResponse))
+
      throw new ApplicationException("unexpected response: module element not found");
+

         
+
    if (!moduleResponse.TryGetProperty(method, out var methodResponse))
+
      throw new ApplicationException("unexpected response: method element not found");
+

         
+
    var sysInfo = JsonSerializer.Deserialize<SysInfo>(methodResponse.GetRawText());
+

         
+
    Console.WriteLine($@"power state: {(sysInfo.relay_state == 0 ? "off" : "on")}");
+
    Console.WriteLine($@"power on time: {sysInfo.on_time} [secs]");
+
  }
+
}
+
}}
+

         
+
#prompt(実行結果・オンの場合){{
+
power state: on
+
power on time: 118 [secs]
+
}}
+

         
+
#prompt(実行結果・オフの場合){{
+
power state: off
+
power on time: 0 [secs]
+
}}
+

         
+

         
+

         
+

         
+
*クライアント実装 [#client_implementation]
+
APIクライアント``SimpleClient``およびコマンドの暗号化・レスポンスの復号を行う``CryptoUtils``のソースコード。 [[MITライセンス>misc/license/MIT]]。
+

         
+

         
+

         
+
APIクライアント実装``SimpleClient``は、コンストラクタに操作したいデバイスのIPアドレスまたはホスト名を指定してインスタンスを作成する。 ネットワークからデバイスを検出する機能は実装していないので、''対象デバイスのIPアドレスまたはホスト名は固定されているか、別の手段で既知である必要がある''。
+

         
+
``SimpleClient``の``SendAsync``メソッドでデバイスに対してコマンドを送信する。 引数として、モジュール名``module``、メソッド名``method``の2つと、オプションでkey-value形式のメソッドパラメータ``methodParameters``を指定する。 レスポンスは&msdn(netfx,type,System.Text.Json.JsonDocument);で返される。
+

         
+
#code(source=SimpleClient.cs,license=mit,copyrightyear=2020)
+
#code(source=CryptoUtils.cs,license=mit,copyrightyear=2020)
+

         
+

         
+

         
+

         
+
*変更履歴
+
**2020-04-26
+
初版
+
-JSON形式のコマンド送信・レスポンス受信を実装、TCPのみサポート
+

         
+