Skip to content
This repository was archived by the owner on May 16, 2024. It is now read-only.

Commit 9b728ba

Browse files
committed
Read the user's date format setting after logging in, because this changes the API output format
1 parent 20d6279 commit 9b728ba

8 files changed

Lines changed: 176 additions & 63 deletions

File tree

src/CodeCaster.GoodWe/GoodWeClient.cs

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
34
using System.IO;
5+
using System.Linq;
46
using System.Net.Http;
57
using System.Net.Http.Json;
68
using System.Text.Json;
@@ -25,8 +27,11 @@ public class GoodWeClient
2527
private const string LoginEndpoint = "/api/v1/Common/CrossLogin";
2628
private const string DefaultToken = @"{""version"":""v2.0.4"",""client"":""ios"",""language"":""en""}";
2729

30+
private const string DateFormatSettingsEndpoint = "/api/v2/Common/GetDateFormatSettingList";
31+
2832
private string _token = DefaultToken;
2933
private readonly JsonSerializerOptions _responseLogJsonFormat = new() { WriteIndented = true };
34+
private JsonSerializerOptions? _serializerOptions;
3035

3136
// TODO: HttpClientFactory injection, and do we need to dispose/recreate on .NET 6 or not (DNS changes)?
3237
private readonly HttpClient _client;
@@ -68,7 +73,7 @@ public async Task<GoodWeApiResponse<ReportData>> GetBatchAsync(string plantId, D
6873

6974
_logger.LogDebug("Getting statistics data with parameters {request}", JsonSerializer.SerializeToDocument(request).RootElement.ToString());
7075

71-
var response = await TryRequest<ReportData>(() => _client.PostAsJsonAsync(endpoint, request, cancellationToken), cancellationToken);
76+
var response = await TryRequest<ReportData>(() => _client.PostAsJsonAsync(endpoint, request, _serializerOptions, cancellationToken), cancellationToken);
7277

7378
await WriteJson("ReportData", response);
7479

@@ -166,14 +171,14 @@ public async Task<GoodWeApiResponse<InverterData>> GetPlantListAsync(Cancellatio
166171
try
167172
{
168173
var response = await call();
169-
var localResponseObject = await response.Content.ReadFromJsonAsync<ResponseBase<TResponse?>?>(cancellationToken: cancellationToken);
174+
var localResponseObject = await response.Content.ReadFromJsonAsync<ResponseBase<TResponse?>?>(_serializerOptions, cancellationToken: cancellationToken);
170175
return localResponseObject;
171176
}
172177
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
173178
{
174179
// Cancellation is requested, fail fast
175180
_logger.LogDebug("Cancellation requested, cancelling request");
176-
181+
177182
throw;
178183
}
179184
catch (Exception ex)
@@ -193,41 +198,42 @@ public async Task<GoodWeApiResponse<InverterData>> GetPlantListAsync(Cancellatio
193198

194199
// 100001: "No access, please login."
195200
// 100002: "The authorization has expired, please login again."
196-
if (responseObject?.Code is 100001 or 100002)
201+
if (responseObject?.Code is "100001" or "100002")
197202
{
198-
switch (responseObject.Code)
203+
if (responseObject.Code == "100001")
204+
{
205+
_logger.LogDebug("Unauthorized, logging in");
206+
}
207+
else if (responseObject.Code == "100002")
199208
{
200-
case 100001:
201-
_logger.LogDebug("Unauthorized, logging in");
202-
break;
203-
case 100002:
204-
_logger.LogDebug("Token expired, logging in again");
205-
break;
209+
_logger.LogDebug("Token expired, logging in again");
206210
}
207211

208-
if (!await Login())
212+
if (!await LoginAsync())
209213
{
210214
_logger.LogWarning("Login failed.");
211215
return default;
212216
}
213217

218+
await ReadUserDateFormatAsync();
219+
214220
// Try again after logging in, letting it fail if that fails.
215221
responseObject = await GetResponse();
216222
}
217223

218-
if (responseObject?.Code != 0)
224+
if (responseObject?.Code != "0")
219225
{
220226
var jsonString = responseObject != null ? JsonSerializer.Serialize(responseObject) : "null";
221-
227+
222228
_logger.LogWarning("Unexpected response: {jsonString}", jsonString);
223-
229+
224230
return default;
225231
}
226232

227233
return responseObject.Data;
228234
}
229235

230-
private async Task<bool> Login()
236+
private async Task<bool> LoginAsync()
231237
{
232238
// Take first and last character...
233239
var logSafeAccount = _accountConfiguration.Account![..1];
@@ -242,17 +248,17 @@ private async Task<bool> Login()
242248

243249
//"code": 100005
244250
//"msg": "Email or password error."
245-
if (responseObject?.Code == 100005)
251+
if (responseObject?.Code == "100005")
246252
{
247253
// TODO: handle login errors
248254
_logger.LogError("Login failed: {code}: {msg}", responseObject.Code, responseObject.Msg);
249-
255+
250256
return false;
251257
}
252-
if (responseObject?.Code != 0)
258+
if (responseObject?.Code != "0")
253259
{
254260
_logger.LogError("Unexpected response: {response}", responseObject != null ? JsonSerializer.Serialize(responseObject) : "null");
255-
261+
256262
return false;
257263
}
258264

@@ -269,23 +275,72 @@ private async Task<bool> Login()
269275
//_apiRoot = responseObject.Components.MsgSocketAdr ?? DefaultApiRoot;
270276
_client.DefaultRequestHeaders.Remove("Token");
271277
_client.DefaultRequestHeaders.Add("Token", _token);
272-
278+
273279
var logSafeToken = _token.Replace(token.token, "***")
274280
.Replace(token.uid, "***");
275281

276282
_logger.LogDebug("Logged in: {logSafeToken}", logSafeToken);
277-
283+
278284
return true;
279285
}
280286

287+
/// <summary>
288+
/// API output date format depends on user UI settings.
289+
/// </summary>
290+
private async Task ReadUserDateFormatAsync()
291+
{
292+
_logger.LogDebug("Reading date settings");
293+
294+
string endpoint = _apiRoot + DateFormatSettingsEndpoint;
295+
296+
var dateSettingsResponse = await _client.PostAsync(endpoint, null);
297+
var responseObject = await dateSettingsResponse.Content.ReadFromJsonAsync<ResponseBase<DateFormatSettingsList>>();
298+
299+
var selected = responseObject.Data.DateFormats.FirstOrDefault(f => f.isselected);
300+
301+
_logger.LogDebug("Translating date format {selectedDateFormat}", selected.date_text);
302+
303+
var format = _dateFormats[selected.date_text];
304+
305+
_serializerOptions = new JsonSerializerOptions
306+
{
307+
PropertyNameCaseInsensitive = true,
308+
Converters =
309+
{
310+
new DateTimeConverter(format),
311+
new NullableDateTimeConverter(format),
312+
}
313+
};
314+
315+
_logger.LogDebug("Dates will be deserialized using format {format}", format);
316+
}
317+
318+
/// <summary>
319+
/// Map user settings to .NET formats.
320+
/// </summary>
321+
private readonly Dictionary<string, string> _dateFormats = new()
322+
{
323+
{ "dateYmd1", "yyyy'/'MM'/'dd"},
324+
{ "dateYMD", "yyyy'.'MM'.'dd"},
325+
{ "dateYmd2", "yy'/'M'/'d"},
326+
{ "dateDmy1", "dd'/'MM'/'yyyy"},
327+
{ "dateDMY", "dd'.'MM'.'yyyy"},
328+
{ "dateMdy1", "MM'/'dd'/'yyyy"},
329+
{ "dateMDY", "MM'.'dd'.'yyyy"},
330+
{ "dateYDM", "yyyy'/'dd'/'MM"},
331+
{ "dateDmy2", "d'/'M'/'yy"},
332+
{ "dateMdy2", "M'/'d'/'yy"},
333+
{ "dateYdm1", "yy'/'d'/'M"}
334+
};
335+
281336
// TODO: IHttpClientFactory injection
282337
private HttpClient CreateClient()
283338
{
284339
var client = new HttpClient();
285340

286341
// Make sure you change this to something valid, when you do.
287342
client.DefaultRequestHeaders.Add("User-Agent", "PVMaster/2.0.4 (iPhone; iOS 11.4.1; Scale/2.00)");
288-
343+
289344
client.DefaultRequestHeaders.Add("Token", _token);
290345

291346
return client;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
5+
namespace CodeCaster.GoodWe.Json
6+
{
7+
/// <summary>
8+
/// The Code property from a GoodWe API response can be an int (100001: No access, please login.), an int in a string ("0"), or an error string ("innerexception").
9+
///
10+
/// This converter converts them all to string. Without it, System.Text.Json complains about an int (100001, 1000002, 100005, ...) not being a string.
11+
/// </summary>
12+
public class CodeConverter : JsonConverter<string>
13+
{
14+
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
15+
{
16+
if (reader.TokenType == JsonTokenType.String)
17+
{
18+
return reader.GetString() ?? throw new ArgumentNullException(null, "Unexpected empty JSON token");
19+
}
20+
else if (reader.TokenType == JsonTokenType.Number)
21+
{
22+
return reader.GetInt32().ToString();
23+
}
24+
25+
throw new JsonException($"Cannot convert token type {reader.TokenType} to string or int");
26+
}
27+
28+
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
29+
{
30+
writer.WriteStringValue(value);
31+
}
32+
}
33+
}

src/CodeCaster.GoodWe/Json/DateTimeConverter.cs

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,60 @@
33
using System.Text.Json;
44
using System.Text.Json.Serialization;
55

6+
// How does one convert DateTime and DateTime? in a generic way? Through copy-paste it seems.
67
namespace CodeCaster.GoodWe.Json
7-
{
8-
public class SystemTextDateTimeConverter : JsonConverter<DateTime?>
8+
{
9+
public class NullableDateTimeConverter : JsonConverter<DateTime?>
910
{
11+
// The API seems to accept this format despite user settings.
12+
private const string defaultWriteFormat = "MM'/'dd'/'yyyy";
13+
1014
private readonly string _format;
1115

12-
public SystemTextDateTimeConverter() : this("MM'/'dd'/'yyyy") { }
13-
14-
public SystemTextDateTimeConverter(string format) { _format = format; }
16+
public NullableDateTimeConverter(string format)
17+
{
18+
_format = format;
19+
}
1520

1621
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
1722
{
1823
var dateString = reader.GetString();
19-
20-
if (!string.IsNullOrWhiteSpace(dateString)
21-
&& DateTime.TryParseExact(dateString, _format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTime))
22-
{
23-
return dateTime;
24-
}
25-
26-
return null;
24+
25+
return string.IsNullOrWhiteSpace(dateString)
26+
? null
27+
: DateTime.ParseExact(dateString, _format, CultureInfo.InvariantCulture, DateTimeStyles.None);
2728
}
2829

2930
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
3031
{
31-
writer.WriteStringValue(value?.ToUniversalTime().ToString(_format));
32+
writer.WriteStringValue(value?.ToUniversalTime().ToString(defaultWriteFormat));
33+
}
34+
}
35+
36+
public class DateTimeConverter : JsonConverter<DateTime>
37+
{
38+
// The API seems to accept this format despite user settings.
39+
private const string defaultWriteFormat = "MM'/'dd'/'yyyy";
40+
41+
private readonly string _format;
42+
43+
public DateTimeConverter(string format)
44+
{
45+
_format = format;
46+
}
47+
48+
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
49+
{
50+
var dateString = reader.GetString();
51+
52+
return string.IsNullOrWhiteSpace(dateString)
53+
? throw new ArgumentNullException(null, "Unexpected empty token")
54+
: DateTime.ParseExact(dateString, _format, CultureInfo.InvariantCulture, DateTimeStyles.None);
55+
}
56+
57+
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
58+
{
59+
writer.WriteStringValue(value.ToUniversalTime().ToString(defaultWriteFormat));
3260
}
3361
}
3462
}

src/CodeCaster.GoodWe/Json/GoodWeRequests.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,7 @@ public ReportRequest(string ids, DateTime start, DateTime? end)
5757
public string Ids { get; set; }
5858
public int Range { get; set; }
5959
public int Type { get; set; }
60-
[JsonConverter(typeof(SystemTextDateTimeConverter))]
6160
public DateTime? Start { get; set; }
62-
[JsonConverter(typeof(SystemTextDateTimeConverter))]
6361
public DateTime? End { get; set; }
6462
public int PageIndex { get; set; }
6563
public int PageSize { get; set; }

0 commit comments

Comments
 (0)