Skip to content

Smdn.Net.EchonetLite.RouteB version 2.0.0

Compare
Choose a tag to compare
@smdn smdn released this 11 Jan 20:57
· 12 commits to main since this release
410f194

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.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-net6.0.apilist.cs b/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-net6.0.apilist.cs
deleted file mode 100644
index 1282c31..0000000
--- a/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-net6.0.apilist.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-// Smdn.Net.EchonetLite.RouteB.dll (Smdn.Net.EchonetLite.RouteB-2.0.0-preview3)
-//   Name: Smdn.Net.EchonetLite.RouteB
-//   AssemblyVersion: 2.0.0.0
-//   InformationalVersion: 2.0.0-preview3+2612dc0eb7dba458048cbe65c5e156d272f8ee87
-//   TargetFramework: .NETCoreApp,Version=v6.0
-//   Configuration: Release
-//   Referenced assemblies:
-//     Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
-//     Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
-//     Smdn.Net.EchonetLite, Version=2.0.0.0, Culture=neutral
-//     Smdn.Net.EchonetLite.Primitives, Version=2.0.0.0, Culture=neutral
-//     Smdn.Net.EchonetLite.RouteB.Primitives, Version=2.0.0.0, Culture=neutral
-//     System.Collections, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-//     System.ComponentModel, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-//     System.Linq, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-//     System.Memory, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
-//     System.Net.Primitives, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-//     System.Runtime, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-#nullable enable annotations
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Smdn.Net.EchonetLite;
-using Smdn.Net.EchonetLite.ObjectModel;
-using Smdn.Net.EchonetLite.RouteB;
-using Smdn.Net.EchonetLite.RouteB.Credentials;
-using Smdn.Net.EchonetLite.RouteB.Transport;
-
-namespace Smdn.Net.EchonetLite.RouteB {
-  public class HemsController :
-    IAsyncDisposable,
-    IDisposable,
-    IRouteBCredentialIdentity
-  {
-    public HemsController(IRouteBEchonetLiteHandlerFactory echonetLiteHandlerFactory, IRouteBCredentialProvider routeBCredentialProvider, ILoggerFactory? loggerFactory = null) {}
-    public HemsController(IServiceProvider serviceProvider) {}
-
-    protected EchonetClient Client { get; }
-    public EchonetObject Controller { get; }
-    [MemberNotNullWhen(false, "echonetLiteHandler")]
-    protected bool IsDisposed { [MemberNotNullWhen(false, "echonetLiteHandler")] get; }
-    public LowVoltageSmartElectricEnergyMeter SmartMeter { get; }
-    public TimeSpan TimeoutWaitingProactiveNotification { get; set; }
-    public TimeSpan TimeoutWaitingResponse1 { get; set; }
-    public TimeSpan TimeoutWaitingResponse2 { get; set; }
-
-    public ValueTask ConnectAsync(CancellationToken cancellationToken = default) {}
-    public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default) {}
-    protected virtual void Dispose(bool disposing) {}
-    public void Dispose() {}
-    public async ValueTask DisposeAsync() {}
-    protected virtual async ValueTask DisposeAsyncCore() {}
-    [MemberNotNull("client")]
-    [MemberNotNull("Client")]
-    [MemberNotNull("smartMeterObject")]
-    [MemberNotNull("controllerObject")]
-    protected void ThrowIfDisconnected() {}
-  }
-
-  public sealed class LowVoltageSmartElectricEnergyMeter : DeviceSuperClass {
-    public IEchonetPropertyGetAccessor<int> Coefficient { get; }
-    public IEchonetPropertyGetAccessor<IReadOnlyList<(MeasurementValue<ElectricEnergyValue> NormalDirection, MeasurementValue<ElectricEnergyValue> ReverseDirection)>> CumulativeElectricEnergyLog2 { get; }
-    public IEchonetPropertySetGetAccessor<DateTime> DayForTheHistoricalDataOfCumulativeElectricEnergy1 { get; }
-    public IEchonetPropertySetGetAccessor<(DateTime DateAndTime, int NumberOfItems)> DayForTheHistoricalDataOfCumulativeElectricEnergy2 { get; }
-    public IEchonetPropertyGetAccessor<(ElectricCurrentValue RPhase, ElectricCurrentValue TPhase)> InstantaneousCurrent { get; }
-    public IEchonetPropertyGetAccessor<int> InstantaneousElectricPower { get; }
-    public IEchonetPropertyGetAccessor<ElectricEnergyValue> NormalDirectionCumulativeElectricEnergy { get; }
-    public IEchonetPropertyGetAccessor<MeasurementValue<ElectricEnergyValue>> NormalDirectionCumulativeElectricEnergyAtEvery30Min { get; }
-    public IEchonetPropertyGetAccessor<IReadOnlyList<MeasurementValue<ElectricEnergyValue>>> NormalDirectionCumulativeElectricEnergyLog1 { get; }
-    public IEchonetPropertyGetAccessor<int> NumberOfEffectiveDigitsCumulativeElectricEnergy { get; }
-    public IEchonetPropertyGetAccessor<(MeasurementValue<ElectricEnergyValue> NormalDirection, MeasurementValue<ElectricEnergyValue> ReverseDirection)> OneMinuteMeasuredCumulativeAmountsOfElectricEnergy { get; }
-    public IEchonetPropertyGetAccessor<ElectricEnergyValue> ReverseDirectionCumulativeElectricEnergy { get; }
-    public IEchonetPropertyGetAccessor<MeasurementValue<ElectricEnergyValue>> ReverseDirectionCumulativeElectricEnergyAtEvery30Min { get; }
-    public IEchonetPropertyGetAccessor<IReadOnlyList<MeasurementValue<ElectricEnergyValue>>> ReverseDirectionCumulativeElectricEnergyLog1 { get; }
-    public IEchonetPropertyGetAccessor<ReadOnlyMemory<byte>> RouteBIdentificationNumber { get; }
-    public IEchonetPropertyGetAccessor<decimal> UnitForCumulativeElectricEnergy { get; }
-  }
-
-  public static class MeasurementValue {
-    public static MeasurementValue<TValue> Create<TValue>(TValue @value, DateTime measuredAt) where TValue : struct {}
-  }
-
-  public sealed class RouteBDeviceFactory : IEchonetDeviceFactory {
-    public static RouteBDeviceFactory Instance { get; }
-
-    public RouteBDeviceFactory() {}
-
-    public EchonetDevice? Create(byte classGroupCode, byte classCode, byte instanceCode) {}
-  }
-
-  public readonly struct ElectricCurrentValue {
-    public decimal Amperes { get; }
-    public bool IsValid { get; }
-    public short RawValue { get; }
-
-    public override string ToString() {}
-  }
-
-  public readonly struct ElectricEnergyValue {
-    public static readonly ElectricEnergyValue NoMeasurementData; // = "(no data)"
-    public static readonly ElectricEnergyValue Zero; // = "0 [kWh]"
-
-    public bool IsValid { get; }
-    public decimal KiloWattHours { get; }
-    public int RawValue { get; }
-    public decimal WattHours { get; }
-
-    public override string ToString() {}
-    public bool TryGetValueAsKiloWattHours(out decimal @value) {}
-  }
-
-  public readonly struct MeasurementValue<TValue> where TValue : struct {
-    public MeasurementValue(TValue @value, DateTime measuredAt) {}
-
-    public DateTime MeasuredAt { get; }
-    public TValue Value { get; }
-
-    public void Deconstruct(out TValue @value, out DateTime measuredAt) {}
-    public override string ToString() {}
-  }
-}
-
-namespace Smdn.Net.EchonetLite.RouteB.Credentials {
-  public static class RouteBCredentialServiceCollectionExtensions {
-    public static IServiceCollection AddRouteBCredential(this IServiceCollection services, string id, string password) {}
-    public static IServiceCollection AddRouteBCredentialFromEnvironmentVariable(this IServiceCollection services, string envVarForId, string envVarForPassword) {}
-    public static IServiceCollection AddRouteBCredentialProvider(this IServiceCollection services, IRouteBCredentialProvider credentialProvider) {}
-  }
-}
-
-namespace Smdn.Net.EchonetLite.RouteB.Transport {
-  public static class RouteBEchonetLiteHandlerBuilderServiceCollectionExtensions {
-    public static IServiceCollection AddRouteBHandler(this IServiceCollection services, Action<IRouteBEchonetLiteHandlerBuilder> configure) {}
-  }
-}
-// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.4.1.0.
-// Smdn.Reflection.ReverseGenerating.ListApi.Core v1.3.1.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-net8.0.apilist.cs b/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-net8.0.apilist.cs
index ed3d689..697fa32 100644
--- a/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-net8.0.apilist.cs
+++ b/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-net8.0.apilist.cs
@@ -1,142 +1,158 @@
-// Smdn.Net.EchonetLite.RouteB.dll (Smdn.Net.EchonetLite.RouteB-2.0.0-preview3)
+// Smdn.Net.EchonetLite.RouteB.dll (Smdn.Net.EchonetLite.RouteB-2.0.0)
 //   Name: Smdn.Net.EchonetLite.RouteB
 //   AssemblyVersion: 2.0.0.0
-//   InformationalVersion: 2.0.0-preview3+2612dc0eb7dba458048cbe65c5e156d272f8ee87
+//   InformationalVersion: 2.0.0+3138f40758ea06ba8f2c2eee70c4237b7f1411d1
 //   TargetFramework: .NETCoreApp,Version=v8.0
 //   Configuration: Release
 //   Referenced assemblies:
 //     Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+//     Polly.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=c8a3ffc3f8f825cc
 //     Smdn.Net.EchonetLite, Version=2.0.0.0, Culture=neutral
 //     Smdn.Net.EchonetLite.Primitives, Version=2.0.0.0, Culture=neutral
 //     Smdn.Net.EchonetLite.RouteB.Primitives, Version=2.0.0.0, Culture=neutral
 //     System.Collections, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.ComponentModel, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+//     System.ComponentModel.Primitives, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Linq, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Memory, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 //     System.Net.Primitives, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 #nullable enable annotations
 
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Diagnostics.CodeAnalysis;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Polly;
 using Smdn.Net.EchonetLite;
 using Smdn.Net.EchonetLite.ObjectModel;
 using Smdn.Net.EchonetLite.RouteB;
 using Smdn.Net.EchonetLite.RouteB.Credentials;
 using Smdn.Net.EchonetLite.RouteB.Transport;
 
 namespace Smdn.Net.EchonetLite.RouteB {
   public class HemsController :
     IAsyncDisposable,
     IDisposable,
     IRouteBCredentialIdentity
   {
-    public HemsController(IRouteBEchonetLiteHandlerFactory echonetLiteHandlerFactory, IRouteBCredentialProvider routeBCredentialProvider, ILoggerFactory? loggerFactory = null) {}
+    public HemsController(IRouteBEchonetLiteHandlerFactory echonetLiteHandlerFactory, IRouteBCredentialProvider routeBCredentialProvider, ILogger? logger, ILoggerFactory? loggerFactoryForEchonetClient) {}
     public HemsController(IServiceProvider serviceProvider) {}
 
     protected EchonetClient Client { get; }
     public EchonetObject Controller { get; }
+    [MemberNotNullWhen(true, "client")]
+    [MemberNotNullWhen(true, "smartMeterObject")]
+    public bool IsConnected { [MemberNotNullWhen(true, "client"), MemberNotNullWhen(true, "smartMeterObject")] get; }
     [MemberNotNullWhen(false, "echonetLiteHandler")]
     protected bool IsDisposed { [MemberNotNullWhen(false, "echonetLiteHandler")] get; }
+    protected ILogger? Logger { get; }
     public LowVoltageSmartElectricEnergyMeter SmartMeter { get; }
+    public ISynchronizeInvoke? SynchronizingObject { get; set; }
     public TimeSpan TimeoutWaitingProactiveNotification { get; set; }
     public TimeSpan TimeoutWaitingResponse1 { get; set; }
     public TimeSpan TimeoutWaitingResponse2 { get; set; }
 
-    public ValueTask ConnectAsync(CancellationToken cancellationToken = default) {}
+    public ValueTask ConnectAsync(ResiliencePipeline? resiliencePipelineForServiceRequest = null, CancellationToken cancellationToken = default) {}
     public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default) {}
     protected virtual void Dispose(bool disposing) {}
     public void Dispose() {}
     public async ValueTask DisposeAsync() {}
     protected virtual async ValueTask DisposeAsyncCore() {}
+    public ValueTask<TResult> RunWithResponseWaitTimer1Async<TResult>(Func<CancellationToken, ValueTask<TResult>> asyncAction, TResult resultForTimeout, CancellationToken cancellationToken = default) {}
+    public ValueTask<TResult> RunWithResponseWaitTimer1Async<TResult>(Func<CancellationToken, ValueTask<TResult>> asyncAction, string? messageForTimeoutException = null, CancellationToken cancellationToken = default) {}
+    public ValueTask<TResult> RunWithResponseWaitTimer2Async<TResult>(Func<CancellationToken, ValueTask<TResult>> asyncAction, string? messageForTimeoutException = null, CancellationToken cancellationToken = default) {}
     [MemberNotNull("client")]
-    [MemberNotNull("Client")]
     [MemberNotNull("smartMeterObject")]
-    [MemberNotNull("controllerObject")]
     protected void ThrowIfDisconnected() {}
+    [MemberNotNull("echonetLiteHandler")]
+    protected void ThrowIfDisposed() {}
   }
 
   public sealed class LowVoltageSmartElectricEnergyMeter : DeviceSuperClass {
     public IEchonetPropertyGetAccessor<int> Coefficient { get; }
     public IEchonetPropertyGetAccessor<IReadOnlyList<(MeasurementValue<ElectricEnergyValue> NormalDirection, MeasurementValue<ElectricEnergyValue> ReverseDirection)>> CumulativeElectricEnergyLog2 { get; }
     public IEchonetPropertySetGetAccessor<DateTime> DayForTheHistoricalDataOfCumulativeElectricEnergy1 { get; }
     public IEchonetPropertySetGetAccessor<(DateTime DateAndTime, int NumberOfItems)> DayForTheHistoricalDataOfCumulativeElectricEnergy2 { get; }
     public IEchonetPropertyGetAccessor<(ElectricCurrentValue RPhase, ElectricCurrentValue TPhase)> InstantaneousCurrent { get; }
     public IEchonetPropertyGetAccessor<int> InstantaneousElectricPower { get; }
     public IEchonetPropertyGetAccessor<ElectricEnergyValue> NormalDirectionCumulativeElectricEnergy { get; }
     public IEchonetPropertyGetAccessor<MeasurementValue<ElectricEnergyValue>> NormalDirectionCumulativeElectricEnergyAtEvery30Min { get; }
     public IEchonetPropertyGetAccessor<IReadOnlyList<MeasurementValue<ElectricEnergyValue>>> NormalDirectionCumulativeElectricEnergyLog1 { get; }
     public IEchonetPropertyGetAccessor<int> NumberOfEffectiveDigitsCumulativeElectricEnergy { get; }
     public IEchonetPropertyGetAccessor<(MeasurementValue<ElectricEnergyValue> NormalDirection, MeasurementValue<ElectricEnergyValue> ReverseDirection)> OneMinuteMeasuredCumulativeAmountsOfElectricEnergy { get; }
     public IEchonetPropertyGetAccessor<ElectricEnergyValue> ReverseDirectionCumulativeElectricEnergy { get; }
     public IEchonetPropertyGetAccessor<MeasurementValue<ElectricEnergyValue>> ReverseDirectionCumulativeElectricEnergyAtEvery30Min { get; }
     public IEchonetPropertyGetAccessor<IReadOnlyList<MeasurementValue<ElectricEnergyValue>>> ReverseDirectionCumulativeElectricEnergyLog1 { get; }
     public IEchonetPropertyGetAccessor<ReadOnlyMemory<byte>> RouteBIdentificationNumber { get; }
     public IEchonetPropertyGetAccessor<decimal> UnitForCumulativeElectricEnergy { get; }
   }
 
   public static class MeasurementValue {
     public static MeasurementValue<TValue> Create<TValue>(TValue @value, DateTime measuredAt) where TValue : struct {}
   }
 
   public sealed class RouteBDeviceFactory : IEchonetDeviceFactory {
     public static RouteBDeviceFactory Instance { get; }
 
     public RouteBDeviceFactory() {}
 
     public EchonetDevice? Create(byte classGroupCode, byte classCode, byte instanceCode) {}
   }
 
   public readonly struct ElectricCurrentValue {
+    public ElectricCurrentValue(short rawValue) {}
+
     public decimal Amperes { get; }
     public bool IsValid { get; }
     public short RawValue { get; }
 
     public override string ToString() {}
   }
 
   public readonly struct ElectricEnergyValue {
     public static readonly ElectricEnergyValue NoMeasurementData; // = "(no data)"
     public static readonly ElectricEnergyValue Zero; // = "0 [kWh]"
 
+    public ElectricEnergyValue(int rawValue, decimal multiplierToKiloWattHours) {}
+
     public bool IsValid { get; }
     public decimal KiloWattHours { get; }
     public int RawValue { get; }
     public decimal WattHours { get; }
 
     public override string ToString() {}
     public bool TryGetValueAsKiloWattHours(out decimal @value) {}
   }
 
   public readonly struct MeasurementValue<TValue> where TValue : struct {
     public MeasurementValue(TValue @value, DateTime measuredAt) {}
 
     public DateTime MeasuredAt { get; }
     public TValue Value { get; }
 
     public void Deconstruct(out TValue @value, out DateTime measuredAt) {}
     public override string ToString() {}
   }
 }
 
 namespace Smdn.Net.EchonetLite.RouteB.Credentials {
   public static class RouteBCredentialServiceCollectionExtensions {
     public static IServiceCollection AddRouteBCredential(this IServiceCollection services, string id, string password) {}
     public static IServiceCollection AddRouteBCredentialFromEnvironmentVariable(this IServiceCollection services, string envVarForId, string envVarForPassword) {}
     public static IServiceCollection AddRouteBCredentialProvider(this IServiceCollection services, IRouteBCredentialProvider credentialProvider) {}
   }
 }
 
 namespace Smdn.Net.EchonetLite.RouteB.Transport {
   public static class RouteBEchonetLiteHandlerBuilderServiceCollectionExtensions {
     public static IServiceCollection AddRouteBHandler(this IServiceCollection services, Action<IRouteBEchonetLiteHandlerBuilder> configure) {}
   }
 }
-// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.4.1.0.
+// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.5.0.0.
 // Smdn.Reflection.ReverseGenerating.ListApi.Core v1.3.1.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-netstandard2.1.apilist.cs b/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-netstandard2.1.apilist.cs
index 51b29de..6d15adc 100644
--- a/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-netstandard2.1.apilist.cs
+++ b/doc/api-list/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB-netstandard2.1.apilist.cs
@@ -1,131 +1,145 @@
-// Smdn.Net.EchonetLite.RouteB.dll (Smdn.Net.EchonetLite.RouteB-2.0.0-preview3)
+// Smdn.Net.EchonetLite.RouteB.dll (Smdn.Net.EchonetLite.RouteB-2.0.0)
 //   Name: Smdn.Net.EchonetLite.RouteB
 //   AssemblyVersion: 2.0.0.0
-//   InformationalVersion: 2.0.0-preview3+2612dc0eb7dba458048cbe65c5e156d272f8ee87
+//   InformationalVersion: 2.0.0+3138f40758ea06ba8f2c2eee70c4237b7f1411d1
 //   TargetFramework: .NETStandard,Version=v2.1
 //   Configuration: Release
 //   Referenced assemblies:
 //     Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+//     Polly.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=c8a3ffc3f8f825cc
 //     Smdn.Net.EchonetLite, Version=2.0.0.0, Culture=neutral
 //     Smdn.Net.EchonetLite.Primitives, Version=2.0.0.0, Culture=neutral
 //     Smdn.Net.EchonetLite.RouteB.Primitives, Version=2.0.0.0, Culture=neutral
 //     netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 #nullable enable annotations
 
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Polly;
 using Smdn.Net.EchonetLite;
 using Smdn.Net.EchonetLite.ObjectModel;
 using Smdn.Net.EchonetLite.RouteB;
 using Smdn.Net.EchonetLite.RouteB.Credentials;
 using Smdn.Net.EchonetLite.RouteB.Transport;
 
 namespace Smdn.Net.EchonetLite.RouteB {
   public class HemsController :
     IAsyncDisposable,
     IDisposable,
     IRouteBCredentialIdentity
   {
-    public HemsController(IRouteBEchonetLiteHandlerFactory echonetLiteHandlerFactory, IRouteBCredentialProvider routeBCredentialProvider, ILoggerFactory? loggerFactory = null) {}
+    public HemsController(IRouteBEchonetLiteHandlerFactory echonetLiteHandlerFactory, IRouteBCredentialProvider routeBCredentialProvider, ILogger? logger, ILoggerFactory? loggerFactoryForEchonetClient) {}
     public HemsController(IServiceProvider serviceProvider) {}
 
     protected EchonetClient Client { get; }
     public EchonetObject Controller { get; }
+    public bool IsConnected { get; }
     protected bool IsDisposed { get; }
+    protected ILogger? Logger { get; }
     public LowVoltageSmartElectricEnergyMeter SmartMeter { get; }
+    public ISynchronizeInvoke? SynchronizingObject { get; set; }
     public TimeSpan TimeoutWaitingProactiveNotification { get; set; }
     public TimeSpan TimeoutWaitingResponse1 { get; set; }
     public TimeSpan TimeoutWaitingResponse2 { get; set; }
 
-    public ValueTask ConnectAsync(CancellationToken cancellationToken = default) {}
+    public ValueTask ConnectAsync(ResiliencePipeline? resiliencePipelineForServiceRequest = null, CancellationToken cancellationToken = default) {}
     public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default) {}
     protected virtual void Dispose(bool disposing) {}
     public void Dispose() {}
     public async ValueTask DisposeAsync() {}
     protected virtual async ValueTask DisposeAsyncCore() {}
+    public ValueTask<TResult> RunWithResponseWaitTimer1Async<TResult>(Func<CancellationToken, ValueTask<TResult>> asyncAction, TResult resultForTimeout, CancellationToken cancellationToken = default) {}
+    public ValueTask<TResult> RunWithResponseWaitTimer1Async<TResult>(Func<CancellationToken, ValueTask<TResult>> asyncAction, string? messageForTimeoutException = null, CancellationToken cancellationToken = default) {}
+    public ValueTask<TResult> RunWithResponseWaitTimer2Async<TResult>(Func<CancellationToken, ValueTask<TResult>> asyncAction, string? messageForTimeoutException = null, CancellationToken cancellationToken = default) {}
     protected void ThrowIfDisconnected() {}
+    protected void ThrowIfDisposed() {}
   }
 
   public sealed class LowVoltageSmartElectricEnergyMeter : DeviceSuperClass {
     public IEchonetPropertyGetAccessor<int> Coefficient { get; }
     public IEchonetPropertyGetAccessor<IReadOnlyList<(MeasurementValue<ElectricEnergyValue> NormalDirection, MeasurementValue<ElectricEnergyValue> ReverseDirection)>> CumulativeElectricEnergyLog2 { get; }
     public IEchonetPropertySetGetAccessor<DateTime> DayForTheHistoricalDataOfCumulativeElectricEnergy1 { get; }
     public IEchonetPropertySetGetAccessor<(DateTime DateAndTime, int NumberOfItems)> DayForTheHistoricalDataOfCumulativeElectricEnergy2 { get; }
     public IEchonetPropertyGetAccessor<(ElectricCurrentValue RPhase, ElectricCurrentValue TPhase)> InstantaneousCurrent { get; }
     public IEchonetPropertyGetAccessor<int> InstantaneousElectricPower { get; }
     public IEchonetPropertyGetAccessor<ElectricEnergyValue> NormalDirectionCumulativeElectricEnergy { get; }
     public IEchonetPropertyGetAccessor<MeasurementValue<ElectricEnergyValue>> NormalDirectionCumulativeElectricEnergyAtEvery30Min { get; }
     public IEchonetPropertyGetAccessor<IReadOnlyList<MeasurementValue<ElectricEnergyValue>>> NormalDirectionCumulativeElectricEnergyLog1 { get; }
     public IEchonetPropertyGetAccessor<int> NumberOfEffectiveDigitsCumulativeElectricEnergy { get; }
     public IEchonetPropertyGetAccessor<(MeasurementValue<ElectricEnergyValue> NormalDirection, MeasurementValue<ElectricEnergyValue> ReverseDirection)> OneMinuteMeasuredCumulativeAmountsOfElectricEnergy { get; }
     public IEchonetPropertyGetAccessor<ElectricEnergyValue> ReverseDirectionCumulativeElectricEnergy { get; }
     public IEchonetPropertyGetAccessor<MeasurementValue<ElectricEnergyValue>> ReverseDirectionCumulativeElectricEnergyAtEvery30Min { get; }
     public IEchonetPropertyGetAccessor<IReadOnlyList<MeasurementValue<ElectricEnergyValue>>> ReverseDirectionCumulativeElectricEnergyLog1 { get; }
     public IEchonetPropertyGetAccessor<ReadOnlyMemory<byte>> RouteBIdentificationNumber { get; }
     public IEchonetPropertyGetAccessor<decimal> UnitForCumulativeElectricEnergy { get; }
   }
 
   public static class MeasurementValue {
     public static MeasurementValue<TValue> Create<TValue>(TValue @value, DateTime measuredAt) where TValue : struct {}
   }
 
   public sealed class RouteBDeviceFactory : IEchonetDeviceFactory {
     public static RouteBDeviceFactory Instance { get; }
 
     public RouteBDeviceFactory() {}
 
     public EchonetDevice? Create(byte classGroupCode, byte classCode, byte instanceCode) {}
   }
 
   public readonly struct ElectricCurrentValue {
+    public ElectricCurrentValue(short rawValue) {}
+
     public decimal Amperes { get; }
     public bool IsValid { get; }
     public short RawValue { get; }
 
     public override string ToString() {}
   }
 
   public readonly struct ElectricEnergyValue {
     public static readonly ElectricEnergyValue NoMeasurementData; // = "(no data)"
     public static readonly ElectricEnergyValue Zero; // = "0 [kWh]"
 
+    public ElectricEnergyValue(int rawValue, decimal multiplierToKiloWattHours) {}
+
     public bool IsValid { get; }
     public decimal KiloWattHours { get; }
     public int RawValue { get; }
     public decimal WattHours { get; }
 
     public override string ToString() {}
     public bool TryGetValueAsKiloWattHours(out decimal @value) {}
   }
 
   public readonly struct MeasurementValue<TValue> where TValue : struct {
     public MeasurementValue(TValue @value, DateTime measuredAt) {}
 
     public DateTime MeasuredAt { get; }
     public TValue Value { get; }
 
     public void Deconstruct(out TValue @value, out DateTime measuredAt) {}
     public override string ToString() {}
   }
 }
 
 namespace Smdn.Net.EchonetLite.RouteB.Credentials {
   public static class RouteBCredentialServiceCollectionExtensions {
     public static IServiceCollection AddRouteBCredential(this IServiceCollection services, string id, string password) {}
     public static IServiceCollection AddRouteBCredentialFromEnvironmentVariable(this IServiceCollection services, string envVarForId, string envVarForPassword) {}
     public static IServiceCollection AddRouteBCredentialProvider(this IServiceCollection services, IRouteBCredentialProvider credentialProvider) {}
   }
 }
 
 namespace Smdn.Net.EchonetLite.RouteB.Transport {
   public static class RouteBEchonetLiteHandlerBuilderServiceCollectionExtensions {
     public static IServiceCollection AddRouteBHandler(this IServiceCollection services, Action<IRouteBEchonetLiteHandlerBuilder> configure) {}
   }
 }
-// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.4.1.0.
+// API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.5.0.0.
 // Smdn.Reflection.ReverseGenerating.ListApi.Core v1.3.1.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)

Full changes

Full changes in this release:
diff --git a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB.Credentials/RouteBCredentialServiceCollectionExtensions.cs b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB.Credentials/RouteBCredentialServiceCollectionExtensions.cs
index 43f7f24..834c744 100644
--- a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB.Credentials/RouteBCredentialServiceCollectionExtensions.cs
+++ b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB.Credentials/RouteBCredentialServiceCollectionExtensions.cs
@@ -70,7 +70,7 @@ public static class RouteBCredentialServiceCollectionExtensions {
 #pragma warning restore CA1510
 
     services.TryAdd(
-      ServiceDescriptor.Singleton(typeof(IRouteBCredentialProvider), credentialProvider)
+      ServiceDescriptor.Singleton<IRouteBCredentialProvider>(credentialProvider)
     );
 
     return services;
diff --git a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB.csproj b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB.csproj
index a755ae2..dc91b46 100644
--- a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB.csproj
+++ b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB.csproj
@@ -5,9 +5,9 @@ SPDX-License-Identifier: MIT
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>netstandard2.1;net6.0;net8.0</TargetFrameworks>
+    <TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks>
     <VersionPrefix>2.0.0</VersionPrefix>
-    <VersionSuffix>preview3</VersionSuffix>
+    <VersionSuffix></VersionSuffix>
     <!-- <PackageValidationBaselineVersion>2.0.0</PackageValidationBaselineVersion> -->
     <Nullable>enable</Nullable>
     <RootNamespace/> <!-- empty the root namespace so that the namespace is determined only by the directory name, for code style rule IDE0030 -->
diff --git a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/ElectricCurrentValue.cs b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/ElectricCurrentValue.cs
index 19f4126..cc7874d 100644
--- a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/ElectricCurrentValue.cs
+++ b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/ElectricCurrentValue.cs
@@ -5,7 +5,7 @@ namespace Smdn.Net.EchonetLite.RouteB;
 /// <summary>
 /// 低圧スマート電力量メータより取得された電流(A)の値を表す構造体です。
 /// </summary>
-public readonly struct ElectricCurrentValue {
+public readonly struct ElectricCurrentValue(short rawValue) {
   private const short Underflow = unchecked((short)(ushort)0x_8000);
   private const short Overflow = unchecked((short)(ushort)0x_7FFF);
   private const short NoMeasurementData = unchecked((short)(ushort)0x_7FFE);
@@ -24,7 +24,7 @@ public readonly struct ElectricCurrentValue {
   /// この値は、計測単位が乗算される前の値を表します。
   /// また、エラー等を表す値の場合もそのまま返します。
   /// </summary>
-  public short RawValue { get; }
+  public short RawValue { get; } = rawValue;
 
   /// <summary>
   /// <see cref="ElectricCurrentValue"/>が有効な値を保持しているかどうかを表す<see cref="bool"/>の値を取得します。
@@ -32,11 +32,6 @@ public readonly struct ElectricCurrentValue {
   /// <value><see langword="true"/>の場合、有効な電流値を保持しています。 <see langword="false"/>の場合、「アンダーフロー」・「オーバーフロー」・「計測値なし」のいずれかを表します。</value>
   public bool IsValid => RawValue is not (Underflow or Overflow or NoMeasurementData);
 
-  internal ElectricCurrentValue(short rawValue)
-  {
-    RawValue = rawValue;
-  }
-
   public override string ToString()
     => RawValue switch {
       Underflow => "(underflow)",
diff --git a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/ElectricEnergyValue.cs b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/ElectricEnergyValue.cs
index f59f9e4..5f3b02a 100644
--- a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/ElectricEnergyValue.cs
+++ b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/ElectricEnergyValue.cs
@@ -1,5 +1,7 @@
 // SPDX-FileCopyrightText: 2023 smdn <[email protected]>
 // SPDX-License-Identifier: MIT
+using System;
+
 namespace Smdn.Net.EchonetLite.RouteB;
 
 /// <summary>
@@ -23,7 +25,7 @@ public readonly struct ElectricEnergyValue {
   /// </summary>
   /// <remarks><see cref="IsValid"/>が<see langword="false"/>の場合、<c>0</c>を返します。</remarks>
   /// <see cref="IsValid"/>
-  public decimal WattHours => KiloWattHours / 1_000.0m;
+  public decimal WattHours => KiloWattHours * 1_000.0m;
 
   /// <summary>
   /// ECHONETプロパティ(EPC)から取得される電力量を、そのままの値で取得します。
@@ -39,8 +41,15 @@ public readonly struct ElectricEnergyValue {
 
   private readonly decimal multiplierToKiloWattHours;
 
-  internal ElectricEnergyValue(int rawValue, decimal multiplierToKiloWattHours)
+  public ElectricEnergyValue(int rawValue, decimal multiplierToKiloWattHours)
   {
+    if (rawValue != NoMeasurementDataValue) {
+      if (rawValue < 0)
+        throw new ArgumentOutOfRangeException(paramName: nameof(rawValue), actualValue: rawValue, message: "must be zero or positive number");
+      if (multiplierToKiloWattHours <= 0.0m)
+        throw new ArgumentOutOfRangeException(paramName: nameof(multiplierToKiloWattHours), actualValue: multiplierToKiloWattHours, message: "must be non-zero positive number");
+    }
+
     this.multiplierToKiloWattHours = multiplierToKiloWattHours;
     RawValue = rawValue;
   }
diff --git a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/HemsController.AIF.cs b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/HemsController.AIF.cs
index 27c4960..75685d4 100644
--- a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/HemsController.AIF.cs
+++ b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/HemsController.AIF.cs
@@ -14,7 +14,7 @@ using System.Threading.Tasks;
 
 using Microsoft.Extensions.Logging;
 
-using Smdn.Net.EchonetLite.Specifications;
+using Polly;
 
 namespace Smdn.Net.EchonetLite.RouteB;
 
@@ -23,7 +23,6 @@ partial class HemsController {
 #pragma warning restore IDE0040
   private EchonetClient? client;
   private LowVoltageSmartElectricEnergyMeter? smartMeterObject;
-  private EchonetObject? controllerObject;
 
   protected EchonetClient Client {
     get {
@@ -59,27 +58,6 @@ partial class HemsController {
     }
   }
 
-  /// <summary>
-  /// 現在スマートメーターと接続しているコントローラーに対応するECHONETオブジェクトを取得します。
-  /// </summary>
-  /// <exception cref="InvalidOperationException">
-  /// まだ<see cref="ConnectAsync"/>による接続の確立が行われていないか、
-  /// または<see cref="DisconnectAsync"/>によって切断されています。
-  /// </exception>
-  /// <exception cref="ObjectDisposedException">オブジェクトはすでに破棄されています。</exception>
-  public EchonetObject Controller {
-    get {
-      ThrowIfDisposed();
-      ThrowIfDisconnected();
-
-#if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
-#pragma warning disable CS8603
-#endif
-      return controllerObject;
-#pragma warning restore CS8603
-    }
-  }
-
   /// <summary>
   /// 「応答待ちタイマー1」として定義されるタイムアウト時間を取得・設定します。
   /// </summary>
@@ -144,16 +122,20 @@ partial class HemsController {
 
   private TimeSpan timeoutWaitingProactiveNotification = TimeSpan.FromSeconds(5.0); // as default
 
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
+  [MemberNotNullWhen(true, nameof(client))]
+  [MemberNotNullWhen(true, nameof(smartMeterObject))]
+#endif
+  public bool IsConnected => client is not null && smartMeterObject is not null;
+
 #if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE
   [MemberNotNull(nameof(client))]
-  [MemberNotNull(nameof(Client))]
   [MemberNotNull(nameof(smartMeterObject))]
-  [MemberNotNull(nameof(controllerObject))]
 #pragma warning disable CS8774
 #endif
   protected void ThrowIfDisconnected()
   {
-    if (client is null || smartMeterObject is null || controllerObject is null)
+    if (client is null || smartMeterObject is null)
       throw new InvalidOperationException("The instance is not connected to smart meter yet or has disconnected from the smart meter.");
   }
 #pragma warning restore CS8774
@@ -161,6 +143,9 @@ partial class HemsController {
   /// <summary>
   /// スマートメーターとの通信を行うECHONET Liteノードを立ち上げ、スマートメーターとの接続を確立します。
   /// </summary>
+  /// <param name="resiliencePipelineForServiceRequest">
+  /// サービス要求のECHONET Lite フレームを送信する際に発生した例外から回復するための動作を規定する<see cref="ResiliencePipeline"/>。
+  /// </param>
   /// <param name="cancellationToken">キャンセル要求を監視するためのトークン。 既定値は<see cref="CancellationToken.None"/>です。</param>
   /// <returns>非同期の操作を表す<see cref="ValueTask"/>。</returns>
   /// <exception cref="InvalidOperationException">すでにスマートメーターとの接続が確立しています。</exception>
@@ -169,7 +154,9 @@ partial class HemsController {
   /// 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
   /// 3.1 立ち上げ動作
   /// </seealso>
+  [CLSCompliant(false)] // ResiliencePipeline is not CLS compliant
   public ValueTask ConnectAsync(
+    ResiliencePipeline? resiliencePipelineForServiceRequest = null,
     CancellationToken cancellationToken = default
   )
   {
@@ -180,64 +167,24 @@ partial class HemsController {
       throw new InvalidOperationException("already connected");
 
     return ConnectAsyncCore(
+      resiliencePipelineForServiceRequest ?? ResiliencePipeline.Empty,
       cancellationToken
     );
 #pragma warning restore IDE0046
   }
 
-  /// <seealso href="https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/AIF/lvsm/lvsm_aif_ver1.01.pdf">
-  /// 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
-  /// 2.1 ECHONET オブジェクト(EOJ)
-  /// </seealso>
-  private static (
-    EchonetClient Client,
-    EchonetObject ControllerObject
-  )
-  CreateEchonetObject(
-    IEchonetLiteHandler echonetLiteHandler,
-    ILoggerFactory? loggerFactory
-  )
-  {
-    // > 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
-    // > ※インスタンスコードは 0x01 固定とする。
-    const byte InstanceCodeForController = 0x01;
-
-    var controllerObject = EchonetObject.Create(
-      objectDetail: EchonetDeviceObjectDetail.Controller, // コントローラ (0x05 0xFF)
-      instanceCode: InstanceCodeForController
-    );
-
-    var controllerNode = EchonetNode.CreateSelfNode(
-      nodeProfile: EchonetObject.CreateNodeProfile(transmissionOnly: false),
-      devices: [controllerObject]
-    );
-
-    // EchonetClient and IEchonetLiteHandler are managed its lifetimes separately,
-    // so EchonetClient must not dispose IEchonetLiteHandler
-    const bool ShouldDisposeEchonetLiteHandlerByClient = false;
-
-    var client = new EchonetClient(
-      selfNode: controllerNode,
-      echonetLiteHandler: echonetLiteHandler,
-      shouldDisposeEchonetLiteHandler: ShouldDisposeEchonetLiteHandlerByClient,
-      deviceFactory: RouteBDeviceFactory.Instance,
-      logger: loggerFactory?.CreateLogger<EchonetClient>()
-    );
-
-    return (client, controllerObject);
-  }
-
   private async ValueTask ConnectAsyncCore(
+    ResiliencePipeline resiliencePipelineForServiceRequest,
     CancellationToken cancellationToken
   )
   {
-    var stopwatchForConnection = logger is null ? null : Stopwatch.StartNew();
+    var stopwatchForConnection = Logger is null ? null : Stopwatch.StartNew();
 
     echonetLiteHandler = await echonetLiteHandlerFactory.CreateAsync(
       cancellationToken: cancellationToken
     ).ConfigureAwait(false);
 
-    logger?.LogInformation("Starting the connection sequence ...");
+    Logger?.LogInformation("Starting the connection sequence ...");
 
     try {
       using var credential = credentialProvider.GetCredential(this);
@@ -247,60 +194,79 @@ partial class HemsController {
         cancellationToken: cancellationToken
       ).ConfigureAwait(false);
 
-      using (var scope = logger?.BeginScope("Establishing route-B connection")) {
-        logger?.LogDebug("EchonetLiteHandler: {EchonetLiteHandler}", echonetLiteHandler.GetType().FullName);
+      using (var scope = Logger?.BeginScope("Establishing route-B connection")) {
+        Logger?.LogDebug("EchonetLiteHandler: {EchonetLiteHandler}", echonetLiteHandler.GetType().FullName);
 
         if (echonetLiteHandler.LocalAddress is null)
           throw new InvalidOperationException($"The local address is not set with this handler. ({echonetLiteHandler.GetType().FullName})");
         if (echonetLiteHandler.PeerAddress is null)
           throw new InvalidOperationException($"The peer address is not set with this handler. ({echonetLiteHandler.GetType().FullName})");
 
-        logger?.LogDebug("Local address: {LocalAddress}", echonetLiteHandler.LocalAddress);
-        logger?.LogDebug("Peer address: {PeerAddress}", echonetLiteHandler.PeerAddress);
+        Logger?.LogDebug("Local address: {LocalAddress}", echonetLiteHandler.LocalAddress);
+        Logger?.LogDebug("Peer address: {PeerAddress}", echonetLiteHandler.PeerAddress);
       }
 
-      logger?.LogInformation("Route-B connection established.");
+      Logger?.LogInformation("Route-B connection established.");
 
-      (client, controllerObject) = CreateEchonetObject(
-        echonetLiteHandler,
-        loggerFactory
-      );
+      // EchonetClient and IEchonetLiteHandler are managed its lifetimes separately,
+      // so EchonetClient must not dispose IEchonetLiteHandler
+      const bool ShouldDisposeEchonetLiteHandlerByClient = false;
 
-      logger?.LogInformation("Finding smart meter node and device object (this may take a few seconds) ...");
+      client = new EchonetClient(
+        selfNode: controllerNode,
+        echonetLiteHandler: echonetLiteHandler,
+        shouldDisposeEchonetLiteHandler: ShouldDisposeEchonetLiteHandlerByClient,
+        nodeRegistry: nodeRegistry,
+        deviceFactory: RouteBDeviceFactory.Instance,
+        resiliencePipelineForSendingResponseFrame: null, // TODO: make configurable
+        logger: loggerFactoryForEchonetClient?.CreateLogger<EchonetClient>()
+      ) {
+        // share same ISynchronizeInvoke
+        SynchronizingObject = synchronizingObject,
+      };
+
+      Logger?.LogInformation("Finding smart meter node and device object (this may take a few seconds) ...");
+
+      using (var scope = Logger?.BeginScope("Finding smart meter")) {
+        smartMeterObject = FindRegisteredRouteBSmartMeterFromNodeRegistry(
+          smartMeterNodeAddress: echonetLiteHandler.PeerAddress
+        );
 
-      using (var scope = logger?.BeginScope("Finding smart meter")) {
-        smartMeterObject = await WaitForRouteBSmartMeterProactiveNotificationAsync(
+        smartMeterObject ??= await WaitForRouteBSmartMeterProactiveNotificationAsync(
           smartMeterNodeAddress: echonetLiteHandler.PeerAddress,
           cancellationToken: cancellationToken
         ).ConfigureAwait(false);
 
         smartMeterObject ??= await RequestRouteBSmartMeterNotifyInstanceListAsync(
           smartMeterNodeAddress: echonetLiteHandler.PeerAddress,
+          resiliencePipelineForServiceRequest: resiliencePipelineForServiceRequest,
           cancellationToken: cancellationToken
         ).ConfigureAwait(false);
 
         if (smartMeterObject is null)
           throw new TimeoutException("Could not find smart meter device object within the specified time span.");
 
-        logger?.LogDebug(
+        Logger?.LogDebug(
           "Smart meter device object found. (Node: {NodeAddress}, Instance code: 0x{InstanceCode})",
           smartMeterObject.Node.Address,
           smartMeterObject.InstanceCode
         );
       }
 
-      logger?.LogInformation("Acquiring smart meter information (this may take a few seconds) ...");
+      Logger?.LogInformation("Acquiring smart meter information (this may take a few seconds) ...");
 
-      using (var scope = logger?.BeginScope("Acquiring information")) {
+      using (var scope = Logger?.BeginScope("Acquiring information")) {
         await AcquireEchonetLiteAttributeInformationAsync(
+          resiliencePipelineForServiceRequest: resiliencePipelineForServiceRequest,
           cancellationToken: cancellationToken
         ).ConfigureAwait(false);
 
         await AcquireSmartMeterAttributeInformationAsync(
+          resiliencePipelineForServiceRequest: resiliencePipelineForServiceRequest,
           cancellationToken: cancellationToken
         ).ConfigureAwait(false);
 
-        logger?.LogInformation(
+        Logger?.LogInformation(
           "Route-B smart meter ready. (Node: {NodeAddress}, Appendix Release: {Protocol}, Serial Number: {SerialNumber})",
           smartMeterObject.Node.Address,
           SmartMeter.Protocol.TryGetValue(out var protocol) ? protocol : "?",
@@ -319,13 +285,12 @@ partial class HemsController {
 
       echonetLiteHandler = null;
 
-      controllerObject = null;
       smartMeterObject = null;
 
       throw;
     }
 
-    logger?.LogInformation(
+    Logger?.LogInformation(
       "Connection sequence completed. ({ElapsedSeconds:N1} secs)",
       stopwatchForConnection!.Elapsed.TotalSeconds
     );
@@ -359,30 +324,131 @@ partial class HemsController {
       }
     }
     finally {
-      controllerObject = null;
       smartMeterObject = null;
 
       client = null;
       echonetLiteHandler = null;
     }
 
-    logger?.LogInformation("Route B connection closed.");
+    Logger?.LogInformation("Route B connection closed.");
 #pragma warning restore CS8602
   }
 
+  private static async ValueTask<TResult?> RunWithTimeoutAsync<TResult>(
+    TimeSpan timeout,
+    Func<CancellationToken, ValueTask<TResult?>> asyncAction,
+    Func<TResult?>? getResultForTimeout,
+    string? messageForTimeoutException,
+    CancellationToken cancellationToken
+  )
+  {
+    using var ctsTimeout = new CancellationTokenSource(delay: timeout);
+    using var ctsTimeoutOrCancellationRequest = CancellationTokenSource.CreateLinkedTokenSource(
+      ctsTimeout.Token,
+      cancellationToken
+    );
+
+    try {
+      return await asyncAction(
+        ctsTimeoutOrCancellationRequest.Token
+      ).ConfigureAwait(false);
+    }
+    catch (OperationCanceledException ex) {
+      if (ctsTimeout.IsCancellationRequested) {
+        if (getResultForTimeout is not null) {
+          return getResultForTimeout();
+        }
+        else {
+          throw new TimeoutException(
+            string.IsNullOrEmpty(messageForTimeoutException)
+              ? $"The operation did not complete within the specified time period {timeout}."
+              : messageForTimeoutException,
+            ex
+          );
+        }
+      }
+
+      throw;
+    }
+  }
+
   /// <summary>
-  /// 下位層でのネットワーク接続確立を契機とする、スマートメーターからの自発的なインスタンスリスト通知を待機します。
+  /// 「応答待ちタイマー1」として定義される時間でタイムアウトする操作を実行します。
+  /// </summary>
+  /// <typeparam name="TResult"><paramref name="asyncAction"/>の実行結果を表す型。</typeparam>
+  /// <param name="asyncAction">「応答待ちタイマー1」として定義される時間でタイムアウトする、非同期で実行される操作。</param>
+  /// <param name="messageForTimeoutException"><see cref="TimeoutException"/>がスローされる場合に、<see cref="Exception.Message"/>として使用されるメッセージ。</param>
+  /// <param name="cancellationToken">キャンセル要求を監視するためのトークン。 既定値は<see cref="CancellationToken.None"/>です。</param>
+  /// <returns>非同期の操作を表す<see cref="ValueTask{TResult}"/>。</returns>
+  /// <exception cref="TimeoutException">
+  /// <paramref name="asyncAction"/>の操作が実行が、<see cref="TimeoutWaitingResponse1"/>で指定される時間内に完了しませんでした。
+  /// </exception>
+  /// <seealso cref="TimeoutWaitingResponse1"/>
+  public ValueTask<TResult?> RunWithResponseWaitTimer1Async<TResult>(
+    Func<CancellationToken, ValueTask<TResult?>> asyncAction,
+    string? messageForTimeoutException = null,
+    CancellationToken cancellationToken = default
+  )
+    => RunWithTimeoutAsync(
+      timeout: TimeoutWaitingResponse1,
+      asyncAction: asyncAction ?? throw new ArgumentNullException(nameof(asyncAction)),
+      getResultForTimeout: null,
+      messageForTimeoutException: messageForTimeoutException,
+      cancellationToken: cancellationToken
+    );
+
+  /// <summary>
+  /// 「応答待ちタイマー1」として定義される時間でタイムアウトする操作を実行します。
   /// </summary>
+  /// <typeparam name="TResult"><paramref name="asyncAction"/>の実行結果を表す型。</typeparam>
+  /// <param name="asyncAction">「応答待ちタイマー1」として定義される時間でタイムアウトする、非同期で実行される操作。</param>
+  /// <param name="resultForTimeout">操作がタイムアウトした場合に、操作の実行結果として返される値を指定します。</param>
+  /// <param name="cancellationToken">キャンセル要求を監視するためのトークン。 既定値は<see cref="CancellationToken.None"/>です。</param>
+  /// <returns>非同期の操作を表す<see cref="ValueTask{TResult}"/>。</returns>
   /// <remarks>
-  /// このメソッドは、<see cref="TimeoutWaitingProactiveNotification"/>で指定された時間でタイムアウトして結果を返します。
+  /// このバージョンでは、非同期の操作がタイムアウトした場合でも、<see cref="TimeoutException"/>はスローされません。
   /// </remarks>
-  /// <seealso href="https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/AIF/lvsm/lvsm_aif_ver1.01.pdf">
-  /// 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
-  /// 3.1.1 ECHONET Lite ノード立ち上げ処理
-  /// </seealso>
-  private ValueTask<LowVoltageSmartElectricEnergyMeter?> WaitForRouteBSmartMeterProactiveNotificationAsync(
-    IPAddress smartMeterNodeAddress,
-    CancellationToken cancellationToken
+  /// <seealso cref="TimeoutWaitingResponse1"/>
+  public ValueTask<TResult?> RunWithResponseWaitTimer1Async<TResult>(
+    Func<CancellationToken, ValueTask<TResult?>> asyncAction,
+    TResult? resultForTimeout,
+    CancellationToken cancellationToken = default
+  )
+    => RunWithTimeoutAsync(
+      timeout: TimeoutWaitingResponse1,
+      asyncAction: asyncAction ?? throw new ArgumentNullException(nameof(asyncAction)),
+      getResultForTimeout: () => resultForTimeout,
+      messageForTimeoutException: null,
+      cancellationToken: cancellationToken
+    );
+
+  /// <summary>
+  /// 「応答待ちタイマー1」として定義される時間でタイムアウトする操作を実行します。
+  /// </summary>
+  /// <typeparam name="TResult"><paramref name="asyncAction"/>の実行結果を表す型。</typeparam>
+  /// <param name="asyncAction">「応答待ちタイマー1」として定義される時間でタイムアウトする、非同期で実行される操作。</param>
+  /// <param name="messageForTimeoutException"><see cref="TimeoutException"/>がスローされる場合に、<see cref="Exception.Message"/>として使用されるメッセージ。</param>
+  /// <param name="cancellationToken">キャンセル要求を監視するためのトークン。 既定値は<see cref="CancellationToken.None"/>です。</param>
+  /// <returns>非同期の操作を表す<see cref="ValueTask{TResult}"/>。</returns>
+  /// <exception cref="TimeoutException">
+  /// <paramref name="asyncAction"/>の操作が実行が、<see cref="TimeoutWaitingResponse2"/>で指定される時間内に完了しませんでした。
+  /// </exception>
+  /// <seealso cref="TimeoutWaitingResponse2"/>
+  public ValueTask<TResult?> RunWithResponseWaitTimer2Async<TResult>(
+    Func<CancellationToken, ValueTask<TResult?>> asyncAction,
+    string? messageForTimeoutException = null,
+    CancellationToken cancellationToken = default
+  )
+    => RunWithTimeoutAsync(
+      timeout: TimeoutWaitingResponse2,
+      asyncAction: asyncAction ?? throw new ArgumentNullException(nameof(asyncAction)),
+      getResultForTimeout: null,
+      messageForTimeoutException: messageForTimeoutException,
+      cancellationToken: cancellationToken
+    );
+
+  private LowVoltageSmartElectricEnergyMeter? FindRegisteredRouteBSmartMeterFromNodeRegistry(
+    IPAddress smartMeterNodeAddress
   )
   {
     ThrowIfSelfNodeNotReady();
@@ -390,43 +456,55 @@ partial class HemsController {
 #if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
 #pragma warning disable CS8602
 #endif
-    var lvsm = client
-      .OtherNodes
+    return client
+      .NodeRegistry
+      .Nodes
       .FirstOrDefault(n => n.Address.Equals(smartMeterNodeAddress))
       ?.Devices
       ?.OfType<LowVoltageSmartElectricEnergyMeter>()
       ?.FirstOrDefault();
 #pragma warning restore CS8602
+  }
 
-    return lvsm is null
-      ? WaitForRouteBSmartMeterProactiveNotificationAsyncCore()
-      : new(lvsm);
-
-    async ValueTask<LowVoltageSmartElectricEnergyMeter?> WaitForRouteBSmartMeterProactiveNotificationAsyncCore()
+  /// <summary>
+  /// 下位層でのネットワーク接続確立を契機とする、スマートメーターからの自発的なインスタンスリスト通知を待機します。
+  /// </summary>
+  /// <remarks>
+  /// このメソッドは、<see cref="TimeoutWaitingProactiveNotification"/>で指定された時間でタイムアウトして結果を返します。
+  /// </remarks>
+  /// <seealso href="https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/AIF/lvsm/lvsm_aif_ver1.01.pdf">
+  /// 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
+  /// 3.1.1 ECHONET Lite ノード立ち上げ処理
+  /// </seealso>
+  private ValueTask<LowVoltageSmartElectricEnergyMeter?> WaitForRouteBSmartMeterProactiveNotificationAsync(
+    IPAddress smartMeterNodeAddress,
+    CancellationToken cancellationToken
+  )
   {
-      using var ctsTimeout = new CancellationTokenSource(TimeoutWaitingProactiveNotification);
-      using var ctsTimeoutOrCancellation = CancellationTokenSource.CreateLinkedTokenSource(
-        ctsTimeout.Token,
-        cancellationToken
-      );
+    ThrowIfSelfNodeNotReady();
 
-      logger?.LogDebug("Waiting for instance list notification ...");
+    Logger?.LogDebug("Waiting for instance list notification ...");
 
+    return RunWithTimeoutAsync(
+      timeout: TimeoutWaitingProactiveNotification,
+      asyncAction: async ct => {
         var tcs = new TaskCompletionSource<LowVoltageSmartElectricEnergyMeter>();
 
-      try {
-        void HandleInstanceListUpdated(object? sender, EchonetNode node)
+        void HandleInstanceListUpdated(object? sender, EchonetNodeEventArgs e)
         {
-          if (node.Address.Equals(smartMeterNodeAddress)) {
-            var lvsm = node.Devices.OfType<LowVoltageSmartElectricEnergyMeter>().FirstOrDefault();
+          if (e.Node.Address.Equals(smartMeterNodeAddress)) {
+            var lvsm = e.Node.Devices.OfType<LowVoltageSmartElectricEnergyMeter>().FirstOrDefault();
 
             if (lvsm is not null)
               tcs.SetResult(lvsm);
           }
         }
 
+#if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
+#pragma warning disable CS8602
+#endif
         try {
-          using var ctr = cancellationToken.Register(() => _ = tcs.TrySetCanceled(ctsTimeoutOrCancellation.Token));
+          using var ctr = ct.Register(() => _ = tcs.TrySetCanceled(ct));
 
           client.InstanceListUpdated += HandleInstanceListUpdated;
 
@@ -436,16 +514,12 @@ partial class HemsController {
         finally {
           client.InstanceListUpdated -= HandleInstanceListUpdated;
         }
-      }
-      catch (OperationCanceledException) {
-        if (ctsTimeout.IsCancellationRequested) {
-          logger?.LogDebug("{Operation} timed out.", nameof(WaitForRouteBSmartMeterProactiveNotificationAsync));
-          return null; // expected timeout
-        }
-
-        throw;
-      }
-    }
+#pragma warning restore CS8602
+      },
+      getResultForTimeout: static () => null, // expected timeout
+      messageForTimeoutException: null, // not used
+      cancellationToken: cancellationToken
+    );
   }
 
   private class InstanceListNotificationState {
@@ -462,8 +536,9 @@ partial class HemsController {
   /// 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
   /// 3.1.1 ECHONET Lite ノード立ち上げ処理
   /// </seealso>
-  private async ValueTask<LowVoltageSmartElectricEnergyMeter?> RequestRouteBSmartMeterNotifyInstanceListAsync(
+  private ValueTask<LowVoltageSmartElectricEnergyMeter?> RequestRouteBSmartMeterNotifyInstanceListAsync(
     IPAddress smartMeterNodeAddress,
+    ResiliencePipeline resiliencePipelineForServiceRequest,
     CancellationToken cancellationToken
   )
   {
@@ -473,18 +548,10 @@ partial class HemsController {
     // > 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
     // > 2.4.2 応答待ちタイマー
     // OPC=1 (0xD5 インスタンスリスト通知)なので、応答待ちタイマー1を使用する
-    using var ctsResponseWaitTimer1 = new CancellationTokenSource(
-      TimeoutWaitingResponse1
-    );
-
-    using var ctsTimeoutOrCancellation = CancellationTokenSource.CreateLinkedTokenSource(
-      ctsResponseWaitTimer1.Token,
-      cancellationToken
-    );
-
-    try {
-      logger?.LogDebug("Requesting for instance list notification.");
+    Logger?.LogDebug("Requesting for instance list notification.");
 
+    return RunWithResponseWaitTimer1Async(
+      asyncAction: async ct => {
         var instanceListState = new InstanceListNotificationState();
 
 #if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
@@ -506,21 +573,17 @@ partial class HemsController {
 
             return false;
           },
-        cancellationToken: ctsTimeoutOrCancellation.Token,
+          resiliencePipelineForServiceRequest: resiliencePipelineForServiceRequest,
+          cancellationToken: ct,
           state: instanceListState
         ).ConfigureAwait(false);
 #pragma warning restore CS8602
 
         return instanceListState.SmartMeter;
-    }
-    catch (OperationCanceledException) {
-      if (ctsResponseWaitTimer1.IsCancellationRequested) {
-        logger?.LogDebug("{Operation} timed out.", nameof(WaitForRouteBSmartMeterProactiveNotificationAsync));
-        return null; // expected timeout
-      }
-
-      throw;
-    }
+      },
+      resultForTimeout: null,
+      cancellationToken: cancellationToken
+    );
   }
 
   /// <summary>
@@ -534,6 +597,7 @@ partial class HemsController {
   /// 3.1.2 ECHONET Lite 属性情報取得
   /// </seealso>
   private async ValueTask AcquireEchonetLiteAttributeInformationAsync(
+    ResiliencePipeline resiliencePipelineForServiceRequest,
     CancellationToken cancellationToken
   )
   {
@@ -542,13 +606,8 @@ partial class HemsController {
     // > https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/AIF/lvsm/lvsm_aif_ver1.01.pdf
     // > 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
     // > 図 3-2 ECHONET Lite 属性情報取得シーケンス例
-    using var ctsResponseWaitTimer2 = new CancellationTokenSource(TimeoutWaitingResponse2);
-
-    using var ctsTimeoutOrCancellation = CancellationTokenSource.CreateLinkedTokenSource(
-      ctsResponseWaitTimer2.Token,
-      cancellationToken
-    );
-
+    _ = await RunWithResponseWaitTimer2Async(
+      asyncAction: async ct => {
         // > https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/AIF/lvsm/lvsm_aif_ver1.01.pdf
         // > 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
         // > 3.1.2 ECHONET Lite 属性情報取得
@@ -557,21 +616,20 @@ partial class HemsController {
         // > ・ 0x9D:状変アナウンスプロパティマップ
         // > ・ 0x9E:Set プロパティマップ
         // > ・ 0x9F:Get プロパティマップ
-    try {
 #if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
 #pragma warning disable CS8602, CS8604
 #endif
-      _ = await client.AcquirePropertyMapsAsync(
-        device: smartMeterObject,
+        var ret = await smartMeterObject.AcquirePropertyMapsAsync(
           extraPropertyCodes: [smartMeterObject.Protocol.PropertyCode],
-        cancellationToken: cancellationToken
+          resiliencePipelineForServiceRequest: resiliencePipelineForServiceRequest,
+          cancellationToken: ct
         ).ConfigureAwait(false);
 #pragma warning restore CS8602, CS8604
 
-      logger?.LogDebug("Listing the implemented properties of the found smart meter.");
+        Logger?.LogDebug("Listing the implemented properties of the found smart meter.");
 
         foreach (var prop in smartMeterObject.Properties.Values.OrderBy(static p => p.Code)) {
-        logger?.LogDebug(
+          Logger?.LogDebug(
             "EPC 0x{Code:X}{Get}{Set}{Anno}",
             prop.Code,
             prop.CanGet ? " GET" : string.Empty,
@@ -579,13 +637,12 @@ partial class HemsController {
             prop.CanAnnounceStatusChange ? " ANNO" : string.Empty
           );
         }
-    }
-    catch (OperationCanceledException ex) {
-      if (ctsResponseWaitTimer2.IsCancellationRequested)
-        throw new TimeoutException("Timed out while acquiring ECHONET Lite attribute information.", ex);
 
-      throw;
-    }
+        return ret;
+      },
+      messageForTimeoutException: "Timed out while acquiring ECHONET Lite attribute information.",
+      cancellationToken: cancellationToken
+    ).ConfigureAwait(false);
   }
 
   /// <summary>
@@ -599,6 +656,7 @@ partial class HemsController {
   /// 3.1.3 スマート電力量メータ属性情報等取得
   /// </seealso>
   private async ValueTask AcquireSmartMeterAttributeInformationAsync(
+    ResiliencePipeline resiliencePipelineForServiceRequest,
     CancellationToken cancellationToken
   )
   {
@@ -607,14 +665,8 @@ partial class HemsController {
     // > https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/AIF/lvsm/lvsm_aif_ver1.01.pdf
     // > 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
     // > 図 3-3 スマート電力量メータ属性情報等取得シーケンス例
-    using var ctsResponseWaitTimer2 = new CancellationTokenSource(TimeoutWaitingResponse2);
-
-    using var ctsTimeoutOrCancellation = CancellationTokenSource.CreateLinkedTokenSource(
-      ctsResponseWaitTimer2.Token,
-      cancellationToken
-    );
-
-    try {
+    _ = await RunWithResponseWaitTimer2Async(
+      asyncAction: async ct => {
         var getResponse = await SmartMeter.ReadPropertiesAsync(
           readPropertyCodes: [
             // > https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/AIF/lvsm/lvsm_aif_ver1.01.pdf
@@ -635,10 +687,11 @@ partial class HemsController {
             SmartMeter.ReverseDirectionCumulativeElectricEnergyAtEvery30Min.PropertyCode,
           ],
           sourceObject: controllerObject!,
-        cancellationToken: ctsTimeoutOrCancellation.Token
+          resiliencePipeline: resiliencePipelineForServiceRequest,
+          cancellationToken: ct
         ).ConfigureAwait(false);
 
-      logger?.LogDebug("Response: {Response}", getResponse);
+        Logger?.LogDebug("Response: {Response}", getResponse);
 
         if (!getResponse.IsSuccess) {
           // TODO: exception type
@@ -649,28 +702,27 @@ partial class HemsController {
 
 #if false
         foreach (var prop in properties) {
-        logger?.LogDebug("EPC: 0x{Code:X2}, PDC: {PDC}", prop.EPC, prop.PDC);
+          Logger?.LogDebug("EPC: 0x{Code:X2}, PDC: {PDC}", prop.EPC, prop.PDC);
         }
 #endif
-    }
-    catch (OperationCanceledException ex) {
-      if (ctsResponseWaitTimer2.IsCancellationRequested)
-        throw new TimeoutException("Timed out while acquiring smart meter attribute information.", ex);
 
-      throw;
-    }
+        return getResponse;
+      },
+      messageForTimeoutException: "Timed out while acquiring smart meter attribute information.",
+      cancellationToken: cancellationToken
+    ).ConfigureAwait(false);
 
-    logger?.LogDebug(
+    Logger?.LogDebug(
       "Coefficient for converting electric energy (0xD3): {Value}",
       SmartMeter.Coefficient.TryGetValue(out var c) ? c.ToString(provider: null) : "-"
     );
 
-    logger?.LogDebug(
+    Logger?.LogDebug(
       "Unit for electric energy (0xE1): {Value}",
       SmartMeter.UnitForCumulativeElectricEnergy.TryGetValue(out var u) ? u.ToString(provider: null) : "-"
     );
 
-    logger?.LogDebug(
+    Logger?.LogDebug(
       "Multiplier for converting electric energy values to kWh: {Value}",
       SmartMeter.MultiplierForCumulativeElectricEnergy
     );
diff --git a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/HemsController.cs b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/HemsController.cs
index 30dee7a..7d15a96 100644
--- a/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/HemsController.cs
+++ b/src/Smdn.Net.EchonetLite.RouteB/Smdn.Net.EchonetLite.RouteB/HemsController.cs
@@ -1,6 +1,7 @@
 // SPDX-FileCopyrightText: 2023 smdn <[email protected]>
 // SPDX-License-Identifier: MIT
 using System;
+using System.ComponentModel;
 #if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE || SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
 using System.Diagnostics.CodeAnalysis;
 #endif
@@ -11,6 +12,7 @@ using Microsoft.Extensions.Logging;
 
 using Smdn.Net.EchonetLite.RouteB.Credentials;
 using Smdn.Net.EchonetLite.RouteB.Transport;
+using Smdn.Net.EchonetLite.Specifications;
 
 namespace Smdn.Net.EchonetLite.RouteB;
 
@@ -31,14 +33,52 @@ namespace Smdn.Net.EchonetLite.RouteB;
 /// <seealso href="https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/Release/Release_R/Appendix_Release_R.pdf">
 /// APPENDIX ECHONET 機器オブジェクト詳細規定 Release R
 /// </seealso>
-[CLSCompliant(false)]
 public partial class HemsController : IRouteBCredentialIdentity, IDisposable, IAsyncDisposable {
+  private readonly EchonetNodeRegistry nodeRegistry;
   private readonly IRouteBEchonetLiteHandlerFactory echonetLiteHandlerFactory;
   private readonly IRouteBCredentialProvider credentialProvider;
-  private readonly ILoggerFactory? loggerFactory;
-  private readonly ILogger<HemsController>? logger;
+  private readonly ILoggerFactory? loggerFactoryForEchonetClient;
   private RouteBEchonetLiteHandler? echonetLiteHandler;
 
+  private ISynchronizeInvoke? synchronizingObject;
+
+  /// <summary>
+  /// イベントの結果として発行されるイベントハンドラー呼び出しをマーシャリングするために使用する<see cref="ISynchronizeInvoke"/>オブジェクトを取得または設定します。
+  /// </summary>
+  public ISynchronizeInvoke? SynchronizingObject {
+    get {
+      ThrowIfDisposed();
+
+      return synchronizingObject;
+    }
+    set {
+      ThrowIfDisposed();
+
+      synchronizingObject = value;
+
+      // share same ISynchronizeInvoke
+      if (client is not null)
+        client.SynchronizingObject = value;
+    }
+  }
+
+  protected ILogger? Logger { get; }
+
+  /// <summary>
+  /// 現在スマートメーターと接続しているコントローラーに対応するECHONETオブジェクトを取得します。
+  /// </summary>
+  /// <exception cref="ObjectDisposedException">オブジェクトはすでに破棄されています。</exception>
+  public EchonetObject Controller {
+    get {
+      ThrowIfDisposed();
+
+      return controllerObject;
+    }
+  }
+
+  private readonly EchonetObject controllerObject;
+  private readonly EchonetNode controllerNode;
+
 #if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLWHENATTRIBUTE
   [MemberNotNullWhen(false, nameof(echonetLiteHandler))]
 #endif
@@ -51,14 +91,15 @@ public partial class HemsController : IRouteBCredentialIdentity, IDisposable, IA
   /// <paramref name="serviceProvider"/>から必須のサービス<see cref="IRouteBEchonetLiteHandlerFactory"/>を取得できません。
   /// または、<paramref name="serviceProvider"/>から必須のサービス<see cref="IRouteBCredentialProvider"/>を取得できません。
   /// </exception>
-  /// <seealso cref="HemsController(IRouteBEchonetLiteHandlerFactory, IRouteBCredentialProvider, ILoggerFactory?)"/>
+  /// <seealso cref="HemsController(IRouteBEchonetLiteHandlerFactory, IRouteBCredentialProvider, ILogger?, ILoggerFactory?)"/>
   public HemsController(
     IServiceProvider serviceProvider
   )
     : this(
       echonetLiteHandlerFactory: (serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider))).GetRequiredService<IRouteBEchonetLiteHandlerFactory>(),
       routeBCredentialProvider: serviceProvider.GetRequiredService<IRouteBCredentialProvider>(),
-      loggerFactory: serviceProvider.GetService<ILoggerFactory>()
+      logger: serviceProvider.GetService<ILoggerFactory>()?.CreateLogger<HemsController>(),
+      loggerFactoryForEchonetClient: serviceProvider.GetService<ILoggerFactory>()
     )
   {
   }
@@ -71,19 +112,22 @@ public partial class HemsController : IRouteBCredentialIdentity, IDisposable, IA
   /// Bルートでの接続の際に使用するIDおよびパスワードを表す<see cref="IRouteBCredential"/>を取得する、
   /// <see cref="IRouteBCredentialProvider"/>の実装を指定します。
   /// </param>
-  /// <param name="loggerFactory">
-  /// <see cref="HemsController"/>および<see cref="EchonetClient"/>が出力するログの出力先となる
-  /// <see cref="ILogger{T}"/>を作成するための<see cref="ILoggerFactory"/>を指定します。
+  /// <param name="logger">
+  /// <see cref="HemsController"/>が出力するログの出力先となる<see cref="ILogger"/>を指定します。
+  /// </param>
+  /// <param name="loggerFactoryForEchonetClient">
+  /// <see cref="EchonetClient"/>が出力するログの出力先となる<see cref="ILogger{T}"/>を作成するための
+  /// <see cref="ILoggerFactory"/>を指定します。
   /// </param>
   /// <exception cref="ArgumentNullException">
   /// <paramref name="echonetLiteHandlerFactory"/>が<see langword="null"/>です。
   /// または、<paramref name="routeBCredentialProvider"/>が<see langword="null"/>です。
   /// </exception>
-  [CLSCompliant(false)]
   public HemsController(
     IRouteBEchonetLiteHandlerFactory echonetLiteHandlerFactory,
     IRouteBCredentialProvider routeBCredentialProvider,
-    ILoggerFactory? loggerFactory = null
+    ILogger? logger,
+    ILoggerFactory? loggerFactoryForEchonetClient
   )
   {
 #pragma warning disable CA1510
@@ -93,11 +137,27 @@ public partial class HemsController : IRouteBCredentialIdentity, IDisposable, IA
       throw new ArgumentNullException(nameof(routeBCredentialProvider));
 #pragma warning restore CA1510
 
+    nodeRegistry = new();
     this.echonetLiteHandlerFactory = echonetLiteHandlerFactory;
     credentialProvider = routeBCredentialProvider;
-    this.loggerFactory = loggerFactory;
-
-    logger = loggerFactory?.CreateLogger<HemsController>();
+    Logger = logger;
+    this.loggerFactoryForEchonetClient = loggerFactoryForEchonetClient;
+
+    // > https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/AIF/lvsm/lvsm_aif_ver1.01.pdf
+    // > 低圧スマート電力量メータ・HEMS コントローラ間アプリケーション通信インタフェース仕様書 Version 1.01
+    // > 2.1 ECHONET オブジェクト(EOJ)
+    // > ※インスタンスコードは 0x01 固定とする。
+    const byte InstanceCodeForController = 0x01;
+
+    controllerObject = EchonetObject.Create(
+      objectDetail: EchonetDeviceObjectDetail.Controller, // コントローラ (0x05 0xFF)
+      instanceCode: InstanceCodeForController
+    );
+
+    controllerNode = EchonetNode.CreateSelfNode(
+      nodeProfile: EchonetObject.CreateNodeProfile(transmissionOnly: false),
+      devices: [controllerObject]
+    );
   }
 
   public void Dispose()
@@ -121,6 +181,9 @@ public partial class HemsController : IRouteBCredentialIdentity, IDisposable, IA
     if (disposing) {
       echonetLiteHandler?.Dispose();
       echonetLiteHandler = null;
+
+      client?.Dispose();
+      client = null;
     }
 
     IsDisposed = true;
@@ -132,12 +195,17 @@ public partial class HemsController : IRouteBCredentialIdentity, IDisposable, IA
       await echonetLiteHandler.DisposeAsync().ConfigureAwait(false);
 
     echonetLiteHandler = null;
+
+    if (client is not null)
+      await client.DisposeAsync().ConfigureAwait(false);
+
+    client = null;
   }
 
 #if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE
   [MemberNotNull(nameof(echonetLiteHandler))]
 #endif
-  private void ThrowIfDisposed()
+  protected void ThrowIfDisposed()
   {
 #pragma warning disable CA1513
     if (IsDisposed)

Notes

New Contributors

  • @smdn made their first contribution in #1

Full Changelog: releases/Smdn.Net.EchonetLite.RouteB-2.0.0-preview3...releases/Smdn.Net.EchonetLite.RouteB-2.0.0