Skip to content

Commit

Permalink
Use the correct subclass when deserializing characteristics and servi…
Browse files Browse the repository at this point in the history
…ces from disk
  • Loading branch information
Supereg committed Oct 18, 2020
1 parent 75452d7 commit 362a5eb
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 8 deletions.
30 changes: 30 additions & 0 deletions src/lib/Characteristic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,21 @@ describe('Characteristic', () => {
eventOnlyCharacteristic: true,
})
});

it("should serialize characteristic with proper constructor name", () => {
const characteristic = new Characteristic.Name();
characteristic.updateValue("New Name!");

const json = Characteristic.serialize(characteristic);
expect(json).toEqual({
displayName: 'Name',
UUID: '00000023-0000-1000-8000-0026BB765291',
eventOnlyCharacteristic: false,
constructorName: 'Name',
value: 'New Name!',
props: { format: 'string', perms: [ 'pr' ], maxLen: 64 }
});
});
});

describe('#deserialize', () => {
Expand Down Expand Up @@ -511,5 +526,20 @@ describe('Characteristic', () => {
expect(characteristic.props).toEqual(json.props);
expect(characteristic.value).toEqual(json.value);
});

it("should deserialize from json with constructor name", () => {
const json: SerializedCharacteristic = {
displayName: 'Name',
UUID: '00000023-0000-1000-8000-0026BB765291',
eventOnlyCharacteristic: false,
constructorName: 'Name',
value: 'New Name!',
props: { format: 'string', perms: [ Perms.PAIRED_READ ], maxLen: 64 }
};

const characteristic = Characteristic.deserialize(json);

expect(characteristic instanceof Characteristic.Name).toBeTruthy();
});
});
});
29 changes: 24 additions & 5 deletions src/lib/Characteristic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ import {
} from "./definitions";
import { HAPStatus } from "./HAPServer";
import { IdentifierCache } from './model/IdentifierCache';
import { Service } from "./Service";
import { clone } from "./util/clone";
import { HAPConnection } from "./util/eventedhttp";
import { HapStatusError } from './util/hapStatusError';
Expand Down Expand Up @@ -368,9 +369,11 @@ export type CharacteristicChange = {
export interface SerializedCharacteristic {
displayName: string,
UUID: string,
props: CharacteristicProps,
value: Nullable<CharacteristicValue>,
eventOnlyCharacteristic: boolean,
constructorName?: string,

value: Nullable<CharacteristicValue>,
props: CharacteristicProps,
}

export const enum CharacteristicEventTypes {
Expand Down Expand Up @@ -1635,12 +1638,18 @@ export class Characteristic extends EventEmitter {
* @internal used to store characteristic on disk
*/
static serialize(characteristic: Characteristic): SerializedCharacteristic {
let constructorName: string | undefined;
if (characteristic.constructor.name !== "Characteristic") {
constructorName = characteristic.constructor.name;
}

return {
displayName: characteristic.displayName,
UUID: characteristic.UUID,
props: clone({}, characteristic.props),
value: characteristic.value,
eventOnlyCharacteristic: characteristic.UUID === Characteristic.ProgrammableSwitchEvent.UUID, // support downgrades for now
constructorName: constructorName,
value: characteristic.value,
props: clone({}, characteristic.props),
}
};

Expand All @@ -1651,7 +1660,17 @@ export class Characteristic extends EventEmitter {
* @internal used to recreate characteristic from disk
*/
static deserialize(json: SerializedCharacteristic): Characteristic {
const characteristic = new Characteristic(json.displayName, json.UUID, json.props);
let characteristic: Characteristic;

if (json.constructorName && json.constructorName.charAt(0).toUpperCase() === json.constructorName.charAt(0)
&& Characteristic[json.constructorName as keyof (typeof Characteristic)]) { // MUST start with uppercase character and must exist on Characteristic object
const constructor = Characteristic[json.constructorName as keyof (typeof Characteristic)] as { new(): Characteristic };
characteristic = new constructor();
characteristic.displayName = json.displayName;
characteristic.setProps(json.props);
} else {
characteristic = new Characteristic(json.displayName, json.UUID, json.props);
}

characteristic.value = json.value;

Expand Down
27 changes: 26 additions & 1 deletion src/lib/Service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Characteristic, Service, ServiceEventTypes, uuid } from '..';
import { Characteristic, SerializedService, Service, ServiceEventTypes, uuid } from '..';

const createService = () => {
return new Service('Test', uuid.generate('Foo'), 'subtype');
Expand Down Expand Up @@ -94,6 +94,13 @@ describe('Service', () => {
expect(service.optionalCharacteristics).toBeDefined();
expect(json.optionalCharacteristics!.length).toEqual(service.optionalCharacteristics.length);
});

it("should serialize service with proper constructor name", () => {
const service = new Service.Speaker("Speaker Name");

const json = Service.serialize(service);
expect(json.constructorName).toBe("Speaker");
});
});

describe('#deserialize', () => {
Expand Down Expand Up @@ -160,5 +167,23 @@ describe('Service', () => {
expect(service.optionalCharacteristics).toBeDefined();
expect(service.optionalCharacteristics!.length).toEqual(5); // as defined in the Lightbulb service
});

it("should deserialize from json with constructor name", () => {
const json: SerializedService = JSON.parse('{"displayName":"Speaker Name","UUID":"00000113-0000-1000-8000-0026BB765291",' +
'"constructorName":"Speaker","hiddenService":false,"primaryService":false,"characteristics":' +
'[{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291","eventOnlyCharacteristic":false,"constructorName":"Name",' +
'"value":"Speaker Name","props":{"format":"string","perms":["pr"],"maxLen":64}},{"displayName":"Mute",' +
'"UUID":"0000011A-0000-1000-8000-0026BB765291","eventOnlyCharacteristic":false,' +
'"constructorName":"Mute","value":false,"props":{"format":"bool","perms":["ev","pr","pw"]}}],' +
'"optionalCharacteristics":[{"displayName":"Active","UUID":"000000B0-0000-1000-8000-0026BB765291",' +
'"eventOnlyCharacteristic":false,"constructorName":"Active","value":0,"props":{"format":"uint8","perms":["ev","pr","pw"],' +
'"minValue":0,"maxValue":1,"minStep":1}},{"displayName":"Volume","UUID":"00000119-0000-1000-8000-0026BB765291",' +
'"eventOnlyCharacteristic":false,"constructorName":"Volume","value":0,"props":{"format":"uint8","perms":["ev","pr","pw"],' +
'"unit":"percentage","minValue":0,"maxValue":100,"minStep":1}}]}');

const service = Service.deserialize(json);

expect(service instanceof Service.Speaker).toBeTruthy();
});
});
});
23 changes: 22 additions & 1 deletion src/lib/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,14 @@ import Timeout = NodeJS.Timeout;
*/
const MAX_CHARACTERISTICS = 100;

/**
* @internal
*/
export interface SerializedService {
displayName: string,
UUID: string,
subtype?: string,
constructorName?: string,

hiddenService?: boolean,
primaryService?: boolean,
Expand Down Expand Up @@ -142,6 +146,7 @@ export declare interface Service {
* work with these.
*/
export class Service extends EventEmitter {
// Service MUST NOT have any other static variables

// Pattern below is for automatic detection of the section of defined services. Used by the generator
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Expand Down Expand Up @@ -667,11 +672,18 @@ export class Service extends EventEmitter {
* @internal
*/
static serialize(service: Service): SerializedService {
let constructorName: string | undefined;
if (service.constructor.name !== "Service") {
constructorName = service.constructor.name; // TODO test
}

return {
displayName: service.displayName,
UUID: service.UUID,
subtype: service.subtype,

constructorName: constructorName,

hiddenService: service.isHiddenService,
primaryService: service.isPrimaryService,

Expand All @@ -684,7 +696,16 @@ export class Service extends EventEmitter {
* @internal
*/
static deserialize(json: SerializedService): Service {
const service = new Service(json.displayName, json.UUID, json.subtype);
let service: Service;

// TODO test
if (json.constructorName && json.constructorName.charAt(0).toUpperCase() === json.constructorName.charAt(0)
&& Service[json.constructorName as keyof (typeof Service)]) { // MUST start with uppercase character and must exist on Service object
const constructor = Service[json.constructorName as keyof (typeof Service)] as { new(displayName?: string, subtype?: string): Service };
service = new constructor(json.displayName, json.subtype);
} else {
service = new Service(json.displayName, json.UUID, json.subtype);
}

service.isHiddenService = !!json.hiddenService;
service.isPrimaryService = !!json.primaryService;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/definitions/generate-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ function checkWrittenVersion(filePath: string, parsingVersion: number): boolean
function rewriteProperties(className: string, properties: [key: string, value: GeneratedCharacteristic | GeneratedService][]): void {
const filePath = path.resolve(__dirname, "../" + className + ".ts");
if (!fs.existsSync(filePath)) {
throw new Error("File '" + filePath + "' does not exists!");
throw new Error("File '" + filePath + "' does not exist!");
}

const file = fs.readFileSync(filePath, { encoding: "utf8"});
Expand Down

0 comments on commit 362a5eb

Please sign in to comment.