Skip to content

Commit 5711610

Browse files
authored
refactor to @signalk/server-api (#970)
1 parent 029b4ed commit 5711610

File tree

3 files changed

+186
-29
lines changed

3 files changed

+186
-29
lines changed

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@
6363
"@angular/compiler-cli": "21.1.4",
6464
"@angular/language-service": "21.1.4",
6565
"@types/canvas-gauges": "^2.1.8",
66+
"@types/d3": "^7.4.3",
6667
"@types/jasmine": "~3.6.0",
6768
"@types/jasminewd2": "^2.0.9",
6869
"@types/js-quantities": "^1.6.6",
6970
"@types/lodash-es": "^4.17.9",
7071
"@types/node": "^24.1.0",
71-
"@types/d3": "^7.4.3",
7272
"angular-eslint": "21.2.0",
7373
"codelyzer": "^6.0.0",
7474
"eslint": "^9.29.0",
@@ -101,6 +101,7 @@
101101
"@angular/router": "21.1.4",
102102
"@aziham/chartjs-plugin-streaming": "^3.5.1",
103103
"@godind/ng-canvas-gauges": "^6.2.1",
104+
"@signalk/server-api": "^2.22.0",
104105
"@zakj/no-sleep": "^0.13.5",
105106
"chart.js": "^4.5.1",
106107
"chartjs-adapter-date-fns": "^3.0.0",
@@ -117,10 +118,9 @@
117118
"prismjs": "^1.30.0",
118119
"rxjs": "^7.8.2",
119120
"screenfull": "^6.0.2",
121+
"sk-ais-status-plugin": "^1.0.0",
120122
"steelseries": "^2.0.9",
121123
"tslib": "^2.6.2",
122-
"zone.js": "^0.15.1",
123-
"@signalk/server-api": "^2.7.2",
124-
"sk-ais-status-plugin": "^1.0.0"
124+
"zone.js": "^0.15.1"
125125
}
126126
}

src/app/core/services/signalk-history.service.spec.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,41 @@ describe('SignalkHistoryService', () => {
4848
httpMock.expectNone(() => true);
4949
});
5050

51+
it('should pass duration to history values endpoint', async () => {
52+
connectionStub.serverServiceEndpoint$.next({
53+
operation: 2,
54+
message: 'Connected',
55+
serverDescription: 'Signal K',
56+
httpServiceUrl: 'http://localhost:3000/signalk/v1/api/',
57+
WsServiceUrl: 'ws://localhost:3000/signalk/v1/stream'
58+
});
59+
60+
const promise = service.getValues({
61+
paths: 'navigation.speedThroughWater:average',
62+
duration: 'PT10M',
63+
resolution: 1
64+
});
65+
66+
const req = httpMock.expectOne((request) =>
67+
request.url === 'http://localhost:3000/signalk/v2/api/history/values'
68+
);
69+
expect(req.request.method).toBe('GET');
70+
expect(req.request.params.get('paths')).toBe('navigation.speedThroughWater:average');
71+
expect(req.request.params.get('duration')).toBe('PT10M');
72+
expect(req.request.params.get('resolution')).toBe('1');
73+
74+
req.flush({
75+
context: 'vessels.self',
76+
range: { from: '2026-02-16T12:00:00.000Z', to: '2026-02-16T12:10:00.000Z' },
77+
values: [{ path: 'navigation.speedThroughWater', method: 'average' }],
78+
data: [['2026-02-16T12:00:00.000Z', 3.2]]
79+
});
80+
81+
const response = await promise;
82+
expect(response).toBeTruthy();
83+
expect(response?.values[0].method).toBe('average');
84+
});
85+
5186
it('should fetch values from v2 history endpoint with resolution in seconds', async () => {
5287
connectionStub.serverServiceEndpoint$.next({
5388
operation: 2,
@@ -113,6 +148,56 @@ describe('SignalkHistoryService', () => {
113148
expect(response).toBeTruthy();
114149
});
115150

151+
it('should pass duration query to history paths endpoint', async () => {
152+
connectionStub.serverServiceEndpoint$.next({
153+
operation: 2,
154+
message: 'Connected',
155+
serverDescription: 'Signal K',
156+
httpServiceUrl: 'http://localhost:3000/signalk/v1/api/',
157+
WsServiceUrl: 'ws://localhost:3000/signalk/v1/stream'
158+
});
159+
160+
const promise = service.getPaths({ duration: 'PT1H' });
161+
162+
const req = httpMock.expectOne((request) =>
163+
request.url === 'http://localhost:3000/signalk/v2/api/history/paths'
164+
);
165+
expect(req.request.method).toBe('GET');
166+
expect(req.request.params.get('duration')).toBe('PT1H');
167+
168+
req.flush(['navigation.speedThroughWater', 'environment.wind.speedTrue']);
169+
170+
const response = await promise;
171+
expect(response).toEqual(['navigation.speedThroughWater', 'environment.wind.speedTrue']);
172+
});
173+
174+
it('should pass from/to query to history contexts endpoint', async () => {
175+
connectionStub.serverServiceEndpoint$.next({
176+
operation: 2,
177+
message: 'Connected',
178+
serverDescription: 'Signal K',
179+
httpServiceUrl: 'http://localhost:3000/signalk/v1/api/',
180+
WsServiceUrl: 'ws://localhost:3000/signalk/v1/stream'
181+
});
182+
183+
const promise = service.getContexts({
184+
from: '2026-02-16T12:00:00.000Z',
185+
to: '2026-02-16T12:01:00.000Z'
186+
});
187+
188+
const req = httpMock.expectOne((request) =>
189+
request.url === 'http://localhost:3000/signalk/v2/api/history/contexts'
190+
);
191+
expect(req.request.method).toBe('GET');
192+
expect(req.request.params.get('from')).toBe('2026-02-16T12:00:00.000Z');
193+
expect(req.request.params.get('to')).toBe('2026-02-16T12:01:00.000Z');
194+
195+
req.flush(['vessels.self', 'vessels.urn:mrn:signalk:uuid:example']);
196+
197+
const response = await promise;
198+
expect(response).toEqual(['vessels.self', 'vessels.urn:mrn:signalk:uuid:example']);
199+
});
200+
116201
it('should pass numeric zero resolution when provided', async () => {
117202
connectionStub.serverServiceEndpoint$.next({
118203
operation: 2,
@@ -190,4 +275,25 @@ describe('SignalkHistoryService', () => {
190275
const response = await promise;
191276
expect(response).toBeNull();
192277
});
278+
279+
it('should return null when history contexts endpoint returns 500', async () => {
280+
connectionStub.serverServiceEndpoint$.next({
281+
operation: 2,
282+
message: 'Connected',
283+
serverDescription: 'Signal K',
284+
httpServiceUrl: 'http://localhost:3000/signalk/v1/api/',
285+
WsServiceUrl: 'ws://localhost:3000/signalk/v1/stream'
286+
});
287+
288+
const promise = service.getContexts({ duration: 'PT10M' });
289+
290+
const req = httpMock.expectOne((request) =>
291+
request.url === 'http://localhost:3000/signalk/v2/api/history/contexts'
292+
);
293+
expect(req.request.method).toBe('GET');
294+
req.flush({ message: 'Server Error' }, { status: 500, statusText: 'Server Error' });
295+
296+
const response = await promise;
297+
expect(response).toBeNull();
298+
});
193299
});

src/app/core/services/signalk-history.service.ts

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,15 @@ import { Injectable, inject, DestroyRef } from '@angular/core';
22
import { HttpClient, HttpParams } from '@angular/common/http';
33
import { firstValueFrom } from 'rxjs';
44
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5+
import type { AggregateMethod, TimeRangeQueryParams } from '@signalk/server-api/history';
56
import { SignalKConnectionService } from './signalk-connection.service';
67

7-
/**
8-
* Represents a single data point from the History API response.
9-
* The array format: [timestamp_string, value1, value2, ...]
10-
* where each value corresponds to the paths in the request.
11-
*/
12-
export interface IHistoryDataPoint {
13-
timestamp: string;
14-
values: (number | string | null | number[])[];
15-
}
16-
178
/**
189
* Represents a single series metadata from the History API response.
1910
*/
20-
export interface IHistoryValueMetadata {
11+
interface IHistoryValueMetadata {
2112
path: string;
22-
method?: string; // e.g., 'sma', 'avg', 'min', 'max'
13+
method?: AggregateMethod | 'avg'; // keep 'avg' for compatibility with existing backends/tests
2314
}
2415

2516
/**
@@ -36,16 +27,22 @@ export interface IHistoryValuesResponse {
3627
}
3728

3829
/**
39-
* Query parameters for the History API.
30+
* Query parameters supported by the /history/values endpoint in this app.
31+
*
32+
* Extends server-api query params while preserving current app compatibility:
33+
* - allows string `resolution` passthrough (e.g. `PT1S`)
34+
* - requires `paths` for the HTTP endpoint variant used by KIP
4035
*/
41-
export interface IHistoryQueryParams {
42-
from?: string; // ISO 8601 date-time (inclusive)
43-
to?: string; // ISO 8601 date-time (inclusive, defaults to now)
44-
duration?: string; // ISO 8601 duration or milliseconds
45-
paths: string; // Required: comma-separated Signal K paths with optional aggregation
46-
context?: string; // Optional Signal K context, default 'vessels.self'
47-
resolution?: string | number; // Optional: window length in seconds or time expression
48-
}
36+
type IHistoryValuesQueryParams = Partial<TimeRangeQueryParams> & {
37+
paths: string;
38+
context?: string;
39+
resolution?: number | string;
40+
};
41+
42+
/**
43+
* Query parameters supported by history endpoints that only require a time range.
44+
*/
45+
type IHistoryTimeRangeQueryParams = Partial<TimeRangeQueryParams>;
4946

5047
@Injectable({
5148
providedIn: 'root'
@@ -73,7 +70,7 @@ export class SignalkHistoryService {
7370
/**
7471
* Gets paths that have historical data available for the specified time range.
7572
*
76-
* @param {Partial<IHistoryQueryParams>} params - Optional time range parameters.
73+
* @param {IHistoryTimeRangeQueryParams} params - Optional time range parameters.
7774
* - from: Start of the time range (ISO 8601), optional
7875
* - to: End of the time range (ISO 8601), optional
7976
* - duration: Duration of the time range (ISO 8601 or milliseconds), optional
@@ -91,7 +88,7 @@ export class SignalkHistoryService {
9188
*
9289
* @memberof SignalkHistoryService
9390
*/
94-
public async getPaths(params?: Partial<IHistoryQueryParams>): Promise<string[] | null> {
91+
public async getPaths(params?: IHistoryTimeRangeQueryParams): Promise<string[] | null> {
9592
try {
9693
if (!this.historyServiceUrl) {
9794
console.warn('[SignalkHistoryService] No HTTP service URL available');
@@ -127,6 +124,60 @@ export class SignalkHistoryService {
127124
}
128125
}
129126

127+
/**
128+
* Gets contexts that have historical data available for the specified time range.
129+
*
130+
* @param {IHistoryTimeRangeQueryParams} params - Optional time range parameters.
131+
* - from: Start of the time range (ISO 8601), optional
132+
* - to: End of the time range (ISO 8601), optional
133+
* - duration: Duration of the time range (ISO 8601 or milliseconds), optional
134+
*
135+
* @returns {Promise<string[] | null>} Array of Signal K contexts with historical data, or null if the request fails.
136+
*
137+
* @example
138+
* const contexts = await historyService.getContexts({ duration: 'PT1H' });
139+
* if (contexts) {
140+
* console.log('Available contexts:', contexts);
141+
* }
142+
*
143+
* @memberof SignalkHistoryService
144+
*/
145+
public async getContexts(params?: IHistoryTimeRangeQueryParams): Promise<string[] | null> {
146+
try {
147+
if (!this.historyServiceUrl) {
148+
console.warn('[SignalkHistoryService] No HTTP service URL available');
149+
return null;
150+
}
151+
152+
const historyUrl = `${this.historyServiceUrl}history/contexts`;
153+
let httpParams = new HttpParams();
154+
155+
// Build query parameters (time range only)
156+
if (params?.from) {
157+
httpParams = httpParams.set('from', params.from);
158+
}
159+
if (params?.to) {
160+
httpParams = httpParams.set('to', params.to);
161+
}
162+
if (params?.duration) {
163+
httpParams = httpParams.set('duration', params.duration.toString());
164+
}
165+
166+
const fullUrl = `${historyUrl}?${httpParams.toString()}`;
167+
console.log(`[SignalkHistoryService] GET ${fullUrl}`);
168+
169+
const response = await firstValueFrom(
170+
this.http.get<string[]>(historyUrl, { params: httpParams })
171+
);
172+
173+
console.log(`[SignalkHistoryService] Retrieved ${response?.length ?? 0} available contexts`);
174+
return response;
175+
} catch (error) {
176+
console.error('[SignalkHistoryService] History API /contexts request failed:', error);
177+
return null;
178+
}
179+
}
180+
130181
/**
131182
* Fetches historical data from the Signal K History API.
132183
*
@@ -135,7 +186,7 @@ export class SignalkHistoryService {
135186
* signalk-parquet. If no history is available or the API is not installed,
136187
* the request will fail.
137188
*
138-
* @param {IHistoryQueryParams} params - Query parameters for the history request.
189+
* @param {IHistoryValuesQueryParams} params - Query parameters for the history request.
139190
* - paths (required): comma-separated Signal K paths with optional aggregation suffixes
140191
* (e.g., 'navigation.speedOverGround:sma:5,navigation.speedThroughWater:avg')
141192
* - from, to, duration: define the time range
@@ -159,7 +210,7 @@ export class SignalkHistoryService {
159210
*
160211
* @memberof SignalkHistoryService
161212
*/
162-
public async getValues(params: IHistoryQueryParams): Promise<IHistoryValuesResponse | null> {
213+
public async getValues(params: IHistoryValuesQueryParams): Promise<IHistoryValuesResponse | null> {
163214
try {
164215
if (!this.historyServiceUrl) {
165216
console.warn('[SignalkHistoryService] No HTTP service URL available');

0 commit comments

Comments
 (0)