Skip to content

Smdn.TPSmartHomeDevices.Primitives version 1.0.0-rc1

Pre-release
Pre-release
Compare
Choose a tag to compare
@smdn smdn released this 27 Apr 16:35
· 256 commits to main since this release
ab1db70

Released package

Release notes

The full release notes are available at gist.

Change log

Change log in this release:

API changes

API changes in this release:
diff --git a/doc/api-list/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives-net6.0.apilist.cs b/doc/api-list/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives-net6.0.apilist.cs
new file mode 100644
index 0000000..d39c2be
--- /dev/null
+++ b/doc/api-list/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives-net6.0.apilist.cs
@@ -0,0 +1,105 @@
+// Smdn.TPSmartHomeDevices.Primitives.dll (Smdn.TPSmartHomeDevices.Primitives-1.0.0-rc1)
+//   Name: Smdn.TPSmartHomeDevices.Primitives
+//   AssemblyVersion: 1.0.0.0
+//   InformationalVersion: 1.0.0-rc1+7ce7d5ecfff9471ef9ebdce241166784cce4cbbd
+//   TargetFramework: .NETCoreApp,Version=v6.0
+//   Configuration: Release
+//   Referenced assemblies:
+//     Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+//     System.Net.NetworkInformation, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+//     System.Net.Primitives, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+//     System.Runtime, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+//     System.Text.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+#nullable enable annotations
+
+using System;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Smdn.TPSmartHomeDevices;
+using Smdn.TPSmartHomeDevices.Json;
+
+namespace Smdn.TPSmartHomeDevices {
+  public interface IDeviceEndPoint {
+    ValueTask<EndPoint?> ResolveAsync(CancellationToken cancellationToken = default);
+  }
+
+  public interface IDeviceEndPointFactory<TAddress> where TAddress : notnull {
+    IDeviceEndPoint Create(TAddress address);
+  }
+
+  public interface IDynamicDeviceEndPoint : IDeviceEndPoint {
+    void Invalidate();
+  }
+
+  public static class DeviceEndPoint {
+    public static IDeviceEndPoint Create(EndPoint endPoint) {}
+    public static IDeviceEndPoint Create(IPAddress ipAddress) {}
+    public static IDeviceEndPoint Create(string host) {}
+    public static IDeviceEndPoint Create<TAddress>(TAddress address, IDeviceEndPointFactory<TAddress> endPointFactory) where TAddress : notnull {}
+  }
+
+  public static class DeviceEndPointFactoryServiceCollectionExtensions {
+    public static IServiceCollection AddDeviceEndPointFactory<TAddress>(this IServiceCollection services, IDeviceEndPointFactory<TAddress> endPointFactory) where TAddress : notnull {}
+  }
+
+  public class DeviceEndPointResolutionException : Exception {
+    public DeviceEndPointResolutionException(IDeviceEndPoint deviceEndPoint) {}
+    public DeviceEndPointResolutionException(IDeviceEndPoint deviceEndPoint, string message, Exception? innerException) {}
+
+    public IDeviceEndPoint DeviceEndPoint { get; }
+  }
+
+  public static class IDeviceEndPointExtensions {
+    public static ValueTask<EndPoint> ResolveOrThrowAsync(this IDeviceEndPoint deviceEndPoint, int defaultPort, CancellationToken cancellationToken = default) {}
+  }
+
+  public sealed class StaticDeviceEndPoint : IDeviceEndPoint {
+    public StaticDeviceEndPoint(EndPoint endPoint) {}
+
+    public ValueTask<EndPoint?> ResolveAsync(CancellationToken cancellationToken = default) {}
+    public override string? ToString() {}
+  }
+}
+
+namespace Smdn.TPSmartHomeDevices.Json {
+  public sealed class GeolocationInDecimalDegreesJsonConverter : JsonConverter<decimal?> {
+    public GeolocationInDecimalDegreesJsonConverter() {}
+
+    public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {}
+    public override void Write(Utf8JsonWriter writer, decimal? @value, JsonSerializerOptions options) {}
+  }
+
+  public sealed class MacAddressJsonConverter : JsonConverter<PhysicalAddress> {
+    public MacAddressJsonConverter() {}
+
+    public override PhysicalAddress? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {}
+    public override void Write(Utf8JsonWriter writer, PhysicalAddress @value, JsonSerializerOptions options) {}
+  }
+
+  public sealed class TimeSpanInMinutesJsonConverter : TimeSpanJsonConverter {
+    public TimeSpanInMinutesJsonConverter() {}
+
+    protected override TimeSpan ToTimeSpan(int @value) {}
+  }
+
+  public sealed class TimeSpanInSecondsJsonConverter : TimeSpanJsonConverter {
+    public TimeSpanInSecondsJsonConverter() {}
+
+    protected override TimeSpan ToTimeSpan(int @value) {}
+  }
+
+  public abstract class TimeSpanJsonConverter : JsonConverter<TimeSpan?> {
+    protected TimeSpanJsonConverter() {}
+
+    public override TimeSpan? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {}
+    protected abstract TimeSpan ToTimeSpan(int @value);
+    public override void Write(Utf8JsonWriter writer, TimeSpan? @value, JsonSerializerOptions options) {}
+  }
+}
+// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.2.2.0.
+// Smdn.Reflection.ReverseGenerating.ListApi.Core v1.2.0.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/doc/api-list/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives-netstandard2.0.apilist.cs b/doc/api-list/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives-netstandard2.0.apilist.cs
new file mode 100644
index 0000000..2086cd7
--- /dev/null
+++ b/doc/api-list/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives-netstandard2.0.apilist.cs
@@ -0,0 +1,104 @@
+// Smdn.TPSmartHomeDevices.Primitives.dll (Smdn.TPSmartHomeDevices.Primitives-1.0.0-rc1)
+//   Name: Smdn.TPSmartHomeDevices.Primitives
+//   AssemblyVersion: 1.0.0.0
+//   InformationalVersion: 1.0.0-rc1+7ce7d5ecfff9471ef9ebdce241166784cce4cbbd
+//   TargetFramework: .NETStandard,Version=v2.0
+//   Configuration: Release
+//   Referenced assemblies:
+//     Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+//     System.Text.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+//     System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+//     netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+#nullable enable annotations
+
+using System;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Smdn.TPSmartHomeDevices;
+using Smdn.TPSmartHomeDevices.Json;
+
+namespace Smdn.TPSmartHomeDevices {
+  public interface IDeviceEndPoint {
+    ValueTask<EndPoint?> ResolveAsync(CancellationToken cancellationToken = default);
+  }
+
+  public interface IDeviceEndPointFactory<TAddress> where TAddress : notnull {
+    IDeviceEndPoint Create(TAddress address);
+  }
+
+  public interface IDynamicDeviceEndPoint : IDeviceEndPoint {
+    void Invalidate();
+  }
+
+  public static class DeviceEndPoint {
+    public static IDeviceEndPoint Create(EndPoint endPoint) {}
+    public static IDeviceEndPoint Create(IPAddress ipAddress) {}
+    public static IDeviceEndPoint Create(string host) {}
+    public static IDeviceEndPoint Create<TAddress>(TAddress address, IDeviceEndPointFactory<TAddress> endPointFactory) where TAddress : notnull {}
+  }
+
+  public static class DeviceEndPointFactoryServiceCollectionExtensions {
+    public static IServiceCollection AddDeviceEndPointFactory<TAddress>(this IServiceCollection services, IDeviceEndPointFactory<TAddress> endPointFactory) where TAddress : notnull {}
+  }
+
+  public class DeviceEndPointResolutionException : Exception {
+    public DeviceEndPointResolutionException(IDeviceEndPoint deviceEndPoint) {}
+    public DeviceEndPointResolutionException(IDeviceEndPoint deviceEndPoint, string message, Exception? innerException) {}
+
+    public IDeviceEndPoint DeviceEndPoint { get; }
+  }
+
+  public static class IDeviceEndPointExtensions {
+    public static ValueTask<EndPoint> ResolveOrThrowAsync(this IDeviceEndPoint deviceEndPoint, int defaultPort, CancellationToken cancellationToken = default) {}
+  }
+
+  public sealed class StaticDeviceEndPoint : IDeviceEndPoint {
+    public StaticDeviceEndPoint(EndPoint endPoint) {}
+
+    public ValueTask<EndPoint?> ResolveAsync(CancellationToken cancellationToken = default) {}
+    public override string? ToString() {}
+  }
+}
+
+namespace Smdn.TPSmartHomeDevices.Json {
+  public sealed class GeolocationInDecimalDegreesJsonConverter : JsonConverter<decimal?> {
+    public GeolocationInDecimalDegreesJsonConverter() {}
+
+    public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {}
+    public override void Write(Utf8JsonWriter writer, decimal? @value, JsonSerializerOptions options) {}
+  }
+
+  public sealed class MacAddressJsonConverter : JsonConverter<PhysicalAddress> {
+    public MacAddressJsonConverter() {}
+
+    public override PhysicalAddress? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {}
+    public override void Write(Utf8JsonWriter writer, PhysicalAddress @value, JsonSerializerOptions options) {}
+  }
+
+  public sealed class TimeSpanInMinutesJsonConverter : TimeSpanJsonConverter {
+    public TimeSpanInMinutesJsonConverter() {}
+
+    protected override TimeSpan ToTimeSpan(int @value) {}
+  }
+
+  public sealed class TimeSpanInSecondsJsonConverter : TimeSpanJsonConverter {
+    public TimeSpanInSecondsJsonConverter() {}
+
+    protected override TimeSpan ToTimeSpan(int @value) {}
+  }
+
+  public abstract class TimeSpanJsonConverter : JsonConverter<TimeSpan?> {
+    protected TimeSpanJsonConverter() {}
+
+    public override TimeSpan? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {}
+    protected abstract TimeSpan ToTimeSpan(int @value);
+    public override void Write(Utf8JsonWriter writer, TimeSpan? @value, JsonSerializerOptions options) {}
+  }
+}
+// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.2.2.0.
+// Smdn.Reflection.ReverseGenerating.ListApi.Core v1.2.0.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/doc/api-list/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives-netstandard2.1.apilist.cs b/doc/api-list/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives-netstandard2.1.apilist.cs
new file mode 100644
index 0000000..300a348
--- /dev/null
+++ b/doc/api-list/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives-netstandard2.1.apilist.cs
@@ -0,0 +1,103 @@
+// Smdn.TPSmartHomeDevices.Primitives.dll (Smdn.TPSmartHomeDevices.Primitives-1.0.0-rc1)
+//   Name: Smdn.TPSmartHomeDevices.Primitives
+//   AssemblyVersion: 1.0.0.0
+//   InformationalVersion: 1.0.0-rc1+7ce7d5ecfff9471ef9ebdce241166784cce4cbbd
+//   TargetFramework: .NETStandard,Version=v2.1
+//   Configuration: Release
+//   Referenced assemblies:
+//     Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+//     System.Text.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+//     netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
+#nullable enable annotations
+
+using System;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Smdn.TPSmartHomeDevices;
+using Smdn.TPSmartHomeDevices.Json;
+
+namespace Smdn.TPSmartHomeDevices {
+  public interface IDeviceEndPoint {
+    ValueTask<EndPoint?> ResolveAsync(CancellationToken cancellationToken = default);
+  }
+
+  public interface IDeviceEndPointFactory<TAddress> where TAddress : notnull {
+    IDeviceEndPoint Create(TAddress address);
+  }
+
+  public interface IDynamicDeviceEndPoint : IDeviceEndPoint {
+    void Invalidate();
+  }
+
+  public static class DeviceEndPoint {
+    public static IDeviceEndPoint Create(EndPoint endPoint) {}
+    public static IDeviceEndPoint Create(IPAddress ipAddress) {}
+    public static IDeviceEndPoint Create(string host) {}
+    public static IDeviceEndPoint Create<TAddress>(TAddress address, IDeviceEndPointFactory<TAddress> endPointFactory) where TAddress : notnull {}
+  }
+
+  public static class DeviceEndPointFactoryServiceCollectionExtensions {
+    public static IServiceCollection AddDeviceEndPointFactory<TAddress>(this IServiceCollection services, IDeviceEndPointFactory<TAddress> endPointFactory) where TAddress : notnull {}
+  }
+
+  public class DeviceEndPointResolutionException : Exception {
+    public DeviceEndPointResolutionException(IDeviceEndPoint deviceEndPoint) {}
+    public DeviceEndPointResolutionException(IDeviceEndPoint deviceEndPoint, string message, Exception? innerException) {}
+
+    public IDeviceEndPoint DeviceEndPoint { get; }
+  }
+
+  public static class IDeviceEndPointExtensions {
+    public static ValueTask<EndPoint> ResolveOrThrowAsync(this IDeviceEndPoint deviceEndPoint, int defaultPort, CancellationToken cancellationToken = default) {}
+  }
+
+  public sealed class StaticDeviceEndPoint : IDeviceEndPoint {
+    public StaticDeviceEndPoint(EndPoint endPoint) {}
+
+    public ValueTask<EndPoint?> ResolveAsync(CancellationToken cancellationToken = default) {}
+    public override string? ToString() {}
+  }
+}
+
+namespace Smdn.TPSmartHomeDevices.Json {
+  public sealed class GeolocationInDecimalDegreesJsonConverter : JsonConverter<decimal?> {
+    public GeolocationInDecimalDegreesJsonConverter() {}
+
+    public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {}
+    [...] public override <unknown> Write(...) {}
+  }
+
+  public sealed class MacAddressJsonConverter : JsonConverter<PhysicalAddress> {
+    public MacAddressJsonConverter() {}
+
+    public override PhysicalAddress? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {}
+    [...] public override <unknown> Write(...) {}
+  }
+
+  public sealed class TimeSpanInMinutesJsonConverter : TimeSpanJsonConverter {
+    public TimeSpanInMinutesJsonConverter() {}
+
+    protected override TimeSpan ToTimeSpan(int @value) {}
+  }
+
+  public sealed class TimeSpanInSecondsJsonConverter : TimeSpanJsonConverter {
+    public TimeSpanInSecondsJsonConverter() {}
+
+    protected override TimeSpan ToTimeSpan(int @value) {}
+  }
+
+  public abstract class TimeSpanJsonConverter : JsonConverter<TimeSpan?> {
+    protected TimeSpanJsonConverter() {}
+
+    public override TimeSpan? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {}
+    protected abstract TimeSpan ToTimeSpan(int @value);
+    [...] public override <unknown> Write(...) {}
+  }
+}
+// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.2.2.0.
+// Smdn.Reflection.ReverseGenerating.ListApi.Core v1.2.0.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)

Full changes

Full changes in this release:
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/GeolocationInDecimalDegreesJsonConverter.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/GeolocationInDecimalDegreesJsonConverter.cs
new file mode 100644
index 0000000..3197ab6
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/GeolocationInDecimalDegreesJsonConverter.cs
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Smdn.TPSmartHomeDevices.Json;
+
+public sealed class GeolocationInDecimalDegreesJsonConverter : JsonConverter<decimal?> {
+  private const decimal Scaler = 10000m;
+
+  public override decimal? Read(
+    ref Utf8JsonReader reader,
+    Type typeToConvert,
+    JsonSerializerOptions options
+  )
+    => reader.TokenType == JsonTokenType.Number && reader.TryGetDecimal(out var scaledDecimalDegrees)
+      ? scaledDecimalDegrees / Scaler
+      : null;
+
+  public override void Write(
+    Utf8JsonWriter writer,
+    decimal? value,
+    JsonSerializerOptions options
+  )
+    => throw new NotImplementedException();
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/MacAddressJsonConverter.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/MacAddressJsonConverter.cs
new file mode 100644
index 0000000..d02af38
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/MacAddressJsonConverter.cs
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Net.NetworkInformation;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Smdn.TPSmartHomeDevices.Json;
+
+public sealed class MacAddressJsonConverter : JsonConverter<PhysicalAddress> {
+  public override PhysicalAddress? Read(
+    ref Utf8JsonReader reader,
+    Type typeToConvert,
+    JsonSerializerOptions options
+  )
+  {
+    var str = reader.TokenType switch {
+      JsonTokenType.Null => null,
+      JsonTokenType.String => reader.GetString(),
+      _ => null,
+    };
+
+    if (str is null)
+      return null;
+
+#if SYSTEM_NET_NETWORKINFORMATION_PHYSICALADDRESS_TRYPARSE
+    return PhysicalAddress.TryParse(str, out var ret)
+      ? ret
+      : null;
+#else
+    try {
+      return PhysicalAddress.Parse(str);
+    }
+    catch (FormatException) {
+      return null;
+    }
+#endif
+  }
+
+  public override void Write(
+    Utf8JsonWriter writer,
+    PhysicalAddress value,
+    JsonSerializerOptions options
+  )
+    => throw new NotImplementedException();
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/TimeSpanInMinutesJsonConverter.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/TimeSpanInMinutesJsonConverter.cs
new file mode 100644
index 0000000..c8748c0
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/TimeSpanInMinutesJsonConverter.cs
@@ -0,0 +1,9 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+
+namespace Smdn.TPSmartHomeDevices.Json;
+
+public sealed class TimeSpanInMinutesJsonConverter : TimeSpanJsonConverter {
+  protected override TimeSpan ToTimeSpan(int value) => TimeSpan.FromMinutes(value);
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/TimeSpanInSecondsJsonConverter.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/TimeSpanInSecondsJsonConverter.cs
new file mode 100644
index 0000000..f159f0b
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/TimeSpanInSecondsJsonConverter.cs
@@ -0,0 +1,9 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+
+namespace Smdn.TPSmartHomeDevices.Json;
+
+public sealed class TimeSpanInSecondsJsonConverter : TimeSpanJsonConverter {
+  protected override TimeSpan ToTimeSpan(int value) => TimeSpan.FromSeconds(value);
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/TimeSpanJsonConverter.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/TimeSpanJsonConverter.cs
new file mode 100644
index 0000000..2282a3d
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Json/TimeSpanJsonConverter.cs
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Smdn.TPSmartHomeDevices.Json;
+
+public abstract class TimeSpanJsonConverter : JsonConverter<TimeSpan?> {
+  protected abstract TimeSpan ToTimeSpan(int value);
+
+  public override TimeSpan? Read(
+    ref Utf8JsonReader reader,
+    Type typeToConvert,
+    JsonSerializerOptions options
+  )
+    => reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var value)
+      ? ToTimeSpan(value)
+      : null;
+
+  public override void Write(
+    Utf8JsonWriter writer,
+    TimeSpan? value,
+    JsonSerializerOptions options
+  )
+    => throw new NotImplementedException();
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives.csproj b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives.csproj
new file mode 100644
index 0000000..7b2451f
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices.Primitives.csproj
@@ -0,0 +1,50 @@
+<!--
+SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+SPDX-License-Identifier: MIT
+-->
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFrameworks>netstandard2.0;netstandard2.1;net6.0</TargetFrameworks>
+    <VersionPrefix>1.0.0</VersionPrefix>
+    <VersionSuffix>rc1</VersionSuffix>
+    <!-- <PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion> -->
+    <Nullable>enable</Nullable>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <EnableTrimAnalyzer>false</EnableTrimAnalyzer>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <NoWarn>CS1591;$(NoWarn)</NoWarn> <!-- CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member' -->
+  </PropertyGroup>
+
+  <PropertyGroup Label="assembly attributes">
+    <Description>
+<![CDATA[Provides common types for Smdn.TPSmartHomeDevices.Kasa and Smdn.TPSmartHomeDevices.Tapo, including abstraction interfaces, extension methods and custom JsonConverter's.
+This library does not provide any specific implementations to operate Kasa and Tapo devices.
+]]>
+    </Description>
+    <CopyrightYear>2023</CopyrightYear>
+  </PropertyGroup>
+
+  <PropertyGroup Label="package properties">
+    <PackageTags>tplink-kasa,kasa,tplink-tapo,tapo,common,$(PackageCommonTags)</PackageTags>
+  </PropertyGroup>
+
+  <PropertyGroup Label="StyleCop code analysis">
+    <StyleCopAnalyzersConfigurationFile>..\stylecop.json</StyleCopAnalyzersConfigurationFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <GlobalAnalyzerConfigFiles Include="..\CodeAnalysis.globalconfig" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="System.Text.Json" Version="6.0.0" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="..\Common\System.Threading.Tasks\ValueTaskShim.cs" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/DeviceEndPoint.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/DeviceEndPoint.cs
new file mode 100644
index 0000000..f538395
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/DeviceEndPoint.cs
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Net;
+
+namespace Smdn.TPSmartHomeDevices;
+
+public static class DeviceEndPoint {
+  public static IDeviceEndPoint Create(string host)
+    => new StaticDeviceEndPoint(
+      new DnsEndPoint(
+        host: host ?? throw new ArgumentNullException(nameof(host)),
+        port: 0
+      )
+    );
+
+  public static IDeviceEndPoint Create(IPAddress ipAddress)
+    => new StaticDeviceEndPoint(
+      new IPEndPoint(
+        address: ipAddress ?? throw new ArgumentNullException(nameof(ipAddress)),
+        port: 0
+      )
+    );
+
+  public static IDeviceEndPoint Create(EndPoint endPoint)
+    => new StaticDeviceEndPoint(
+      endPoint ?? throw new ArgumentNullException(nameof(endPoint))
+    );
+
+  public static IDeviceEndPoint Create<TAddress>(
+    TAddress address,
+    IDeviceEndPointFactory<TAddress> endPointFactory
+  ) where TAddress : notnull
+    => (endPointFactory ?? throw new ArgumentNullException(nameof(endPointFactory)))
+      .Create(address: address ?? throw new ArgumentNullException(nameof(address)));
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/DeviceEndPointFactoryServiceCollectionExtensions.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/DeviceEndPointFactoryServiceCollectionExtensions.cs
new file mode 100644
index 0000000..aa67a3e
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/DeviceEndPointFactoryServiceCollectionExtensions.cs
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Smdn.TPSmartHomeDevices;
+
+public static class DeviceEndPointFactoryServiceCollectionExtensions {
+  /// <summary>
+  /// Adds <see cref="IDeviceEndPointFactory{TAddress}"/> to create an <see cref="IDeviceEndPoint"/>
+  /// that uses <typeparamref name="TAddress"/> as the address type to represent the device endpoint.
+  /// </summary>
+  /// <typeparam name="TAddress">The type that represents an address of device endpoint.</typeparam>
+  /// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
+  /// <param name="endPointFactory">The <see cref="IDeviceEndPointFactory{TAddress}"/> that is added to services.</param>
+  public static IServiceCollection AddDeviceEndPointFactory<TAddress>(
+    this IServiceCollection services,
+    IDeviceEndPointFactory<TAddress> endPointFactory
+  ) where TAddress : notnull
+  {
+    if (services is null)
+      throw new ArgumentNullException(nameof(services));
+    if (endPointFactory is null)
+      throw new ArgumentNullException(nameof(endPointFactory));
+
+    services.TryAdd(ServiceDescriptor.Singleton(typeof(IDeviceEndPointFactory<TAddress>), endPointFactory));
+
+    return services;
+  }
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/DeviceEndPointResolutionException.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/DeviceEndPointResolutionException.cs
new file mode 100644
index 0000000..865fd56
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/DeviceEndPointResolutionException.cs
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+
+namespace Smdn.TPSmartHomeDevices;
+
+public class DeviceEndPointResolutionException : Exception {
+  public IDeviceEndPoint DeviceEndPoint { get; }
+
+  public DeviceEndPointResolutionException(
+    IDeviceEndPoint deviceEndPoint
+  )
+    : this(
+      deviceEndPoint: deviceEndPoint,
+      message: "Could not get or resolve the device endpoint.",
+      innerException: null
+    )
+  {
+  }
+
+  public DeviceEndPointResolutionException(
+    IDeviceEndPoint deviceEndPoint,
+    string message,
+    Exception? innerException
+  )
+    : base(
+      message: message,
+      innerException: innerException
+    )
+  {
+    DeviceEndPoint = deviceEndPoint;
+  }
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDeviceEndPoint.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDeviceEndPoint.cs
new file mode 100644
index 0000000..aed078f
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDeviceEndPoint.cs
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Smdn.TPSmartHomeDevices;
+
+/// <summary>
+/// Provides a mechanism for abstracting device endpoints and resolving to specific <seealso cref="EndPoint" />.
+/// </summary>
+/// <seealso cref="DeviceEndPoint" />
+/// <seealso cref="StaticDeviceEndPoint" />
+/// <seealso cref="IDynamicDeviceEndPoint" />
+public interface IDeviceEndPoint {
+  /// <summary>
+  /// Resolves this endpoint to its corresponding <see cref="EndPoint"/>.
+  /// </summary>
+  /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+  /// <returns>
+  /// A <see cref="ValueTask{EndPoint}"/> representing the result of endpoint resolution.
+  /// If the endpoint is successfully resolved, <see cref="EndPoint"/> representing the resolved endpoint is set. If not, <see langword="null" /> is set.
+  /// </returns>
+  /// <seealso cref="IDynamicDeviceEndPoint.Invalidate" />
+  ValueTask<EndPoint?> ResolveAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDeviceEndPointExtensions.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDeviceEndPointExtensions.cs
new file mode 100644
index 0000000..26c07a3
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDeviceEndPointExtensions.cs
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Smdn.TPSmartHomeDevices;
+
+/// <summary>
+/// Provides extension methods for <see cref="IDeviceEndPoint"/>.
+/// </summary>
+/// <seealso cref="IDeviceEndPoint" />
+public static class IDeviceEndPointExtensions {
+  /// <summary>
+  /// Resolves this endpoint to its corresponding <see cref="EndPoint"/>.
+  /// If the endpoint could not resolve to the specific <see cref="EndPoint"/>, throw <see cref="DeviceEndPointResolutionException"/>.
+  /// </summary>
+  /// <remarks>
+  /// This method calls the <see cref="IDynamicDeviceEndPoint.Invalidate" /> if the <paramref name="deviceEndPoint"/> is an <see cref="IDynamicDeviceEndPoint"/> and the endpoint cannot be resolved.
+  /// </remarks>
+  /// <param name="deviceEndPoint">The <see cref="IDeviceEndPoint" />.</param>
+  /// <param name="defaultPort">The default port number. If the resolved <see cref="EndPoint"/> does not specify a specific port, set this port number to the resolved <see cref="EndPoint"/>.</param>
+  /// <param name="cancellationToken">The <see cref="CancellationToken" /> to monitor for cancellation requests.</param>
+  /// <returns>
+  /// A <see cref="ValueTask{EndPoint}"/> representing the result of endpoint resolution.
+  /// </returns>
+  /// <exception cref="DeviceEndPointResolutionException">The endpoint could not resolve to the specific <see cref="EndPoint"/>.</exception>
+  /// <seealso cref="IDeviceEndPoint.ResolveAsync(CancellationToken)" />
+  /// <seealso cref="IDynamicDeviceEndPoint.Invalidate" />
+  public static ValueTask<EndPoint> ResolveOrThrowAsync(
+    this IDeviceEndPoint deviceEndPoint,
+    int defaultPort,
+    CancellationToken cancellationToken = default
+  )
+  {
+    if (deviceEndPoint is null)
+      throw new ArgumentNullException(nameof(deviceEndPoint));
+    if (defaultPort < 0)
+      throw new ArgumentOutOfRangeException(message: "must be positive number", paramName: nameof(defaultPort));
+
+    if (cancellationToken.IsCancellationRequested)
+#if SYSTEM_THREADING_TASKS_VALUETASK_FROMCANCELED
+      return ValueTask.FromCanceled<EndPoint>(cancellationToken);
+#else
+      return ValueTaskShim.FromCanceled<EndPoint>(cancellationToken);
+#endif
+
+    return ResolveOrThrowAsyncCore();
+
+    async ValueTask<EndPoint> ResolveOrThrowAsyncCore()
+    {
+      var endPoint = await deviceEndPoint.ResolveAsync(cancellationToken).ConfigureAwait(false);
+
+      if (endPoint is null) {
+        if (deviceEndPoint is IDynamicDeviceEndPoint dynamicDeviceEndPoint)
+          dynamicDeviceEndPoint.Invalidate();
+
+        throw new DeviceEndPointResolutionException(deviceEndPoint);
+      }
+
+      return endPoint switch {
+        IPEndPoint ipEndPoint => ipEndPoint.Port == 0
+          ? new IPEndPoint(ipEndPoint.Address, defaultPort)
+          : ipEndPoint,
+
+        DnsEndPoint dnsEndPoint => dnsEndPoint.Port == 0
+          ? new DnsEndPoint(dnsEndPoint.Host, defaultPort)
+          : dnsEndPoint,
+
+        /* EndPoint */ _ => endPoint,
+      };
+    }
+  }
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDeviceEndPointFactory.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDeviceEndPointFactory.cs
new file mode 100644
index 0000000..afad3cc
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDeviceEndPointFactory.cs
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+namespace Smdn.TPSmartHomeDevices;
+
+/// <summary>
+/// Provides a mechanism for creating <see cref="IDeviceEndPoint"/> from the address respresented by <typeparamref name="TAddress"/>.
+/// </summary>
+/// <remarks>
+/// <c>MacAddressDeviceEndPointFactory</c> class from the package <c>Smdn.TPSmartHomeDevices.MacAddressEndPoint</c> is one concrete implementation of this interface.
+/// </remarks>
+/// <typeparam name="TAddress">The type that represents an address of device endpoint.</typeparam>
+/// <seealso cref="IDeviceEndPoint" />
+public interface IDeviceEndPointFactory<TAddress> where TAddress : notnull {
+  /// <summary>
+  /// Creates an <see cref="IDeviceEndPoint"/> that is resolved by the address represented by type <typeparamref name="TAddress"/>.
+  /// </summary>
+  /// <param name="address">The address to identify the device endpoint.</param>
+  IDeviceEndPoint Create(TAddress address);
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDynamicDeviceEndPoint.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDynamicDeviceEndPoint.cs
new file mode 100644
index 0000000..c97256b
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/IDynamicDeviceEndPoint.cs
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+namespace Smdn.TPSmartHomeDevices;
+
+/// <summary>
+/// Provides a mechanism for abstracting device endpoints and resolving to specific <seealso cref="System.Net.EndPoint" />.
+/// This interface represents a 'dynamic' endpoint, such as when the actual IP address is assigned by DHCP.
+/// </summary>
+/// <remarks>
+/// <c>MacAddressDeviceEndPointFactory+MacAddressDeviceEndPoint</c> class from the package <c>Smdn.TPSmartHomeDevices.MacAddressEndPoint</c> is one concrete implementation of this interface.
+/// </remarks>
+public interface IDynamicDeviceEndPoint : IDeviceEndPoint {
+  /// <summary>
+  /// Marks the device endpoint as 'invalidated', for example,
+  /// if the resolved <seealso cref="System.Net.EndPoint" /> is unreachable or expired.
+  /// </summary>
+  void Invalidate();
+}
diff --git a/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/StaticDeviceEndPoint.cs b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/StaticDeviceEndPoint.cs
new file mode 100644
index 0000000..5868549
--- /dev/null
+++ b/src/Smdn.TPSmartHomeDevices.Primitives/Smdn.TPSmartHomeDevices/StaticDeviceEndPoint.cs
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: 2023 smdn <smdn@smdn.jp>
+// SPDX-License-Identifier: MIT
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Smdn.TPSmartHomeDevices;
+
+/// <summary>
+/// A concrete implementation of <see cref="IDeviceEndPoint"/>, which represents a device endpoint by <see cref="EndPoint"/>.
+/// </summary>
+public sealed class StaticDeviceEndPoint : IDeviceEndPoint {
+  private readonly EndPoint endPoint;
+
+  public StaticDeviceEndPoint(EndPoint endPoint)
+  {
+    this.endPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint));
+  }
+
+  public ValueTask<EndPoint?> ResolveAsync(CancellationToken cancellationToken = default)
+    => cancellationToken.IsCancellationRequested
+      ?
+#if SYSTEM_THREADING_TASKS_VALUETASK_FROMCANCELED
+        ValueTask.FromCanceled<EndPoint?>(cancellationToken)
+#else
+        ValueTaskShim.FromCanceled<EndPoint?>(cancellationToken)
+#endif
+      : new(endPoint);
+
+  public override string? ToString()
+    => endPoint.ToString();
+}

Notes

Full Changelog: releases/Smdn.TPSmartHomeDevices-1.0.0-preview3...releases/Smdn.TPSmartHomeDevices.Primitives-1.0.0-rc1