Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions src/device/attribute/boolDeviceAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import type { DeviceAttributeModifier, NotJustUndefined, NotUndefined } from './deviceAttribute.js';
import type { DeviceAttributeModifier } from './deviceAttribute.js';
import DeviceAttribute from './deviceAttribute.js';

type BoolDeviceAttributeValue = NotJustUndefined<boolean | undefined>;
export type InitializedBoolDeviceAttribute = BoolDeviceAttribute<true>;

export type InitializedBoolDeviceAttribute = BoolDeviceAttribute<boolean>;

export default class BoolDeviceAttribute<T extends BoolDeviceAttributeValue = BoolDeviceAttributeValue> extends DeviceAttribute<T>
export default class BoolDeviceAttribute<IsInitialized extends boolean = false> extends DeviceAttribute<boolean, IsInitialized>
{
public static createInitialized(
name: string,
label: string | undefined,
modifier: DeviceAttributeModifier,
initialValue: boolean,
): InitializedBoolDeviceAttribute {
return new BoolDeviceAttribute<boolean>(name, label, modifier, initialValue);
return new BoolDeviceAttribute<true>(name, label, modifier, initialValue);
}

public static create(
Expand All @@ -24,13 +22,11 @@ export default class BoolDeviceAttribute<T extends BoolDeviceAttributeValue = Bo
return new BoolDeviceAttribute(name, label, modifier, undefined);
}

public override fromString(value: string): T {
// TODO https://github.com/SlvCtrlPlus/slvctrlplus-server/issues/107
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-type-assertion
return (value === '1') as T;
public override fromString(value: string): boolean {
return value === '1';
}

public override isValidValue(value: unknown): value is NotUndefined<T> {
public override isValidValue(value: unknown): value is boolean {
return typeof value === 'boolean';
}

Expand Down
31 changes: 17 additions & 14 deletions src/device/attribute/deviceAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Exclude, Expose } from 'class-transformer';
import type { Float, Int } from '../../util/numbers.js';

export type NotJustUndefined<V> = [V] extends [undefined] ? never : V;
export type NotUndefined<V> = V extends undefined ? never : V;
export type AttributeValue = NotJustUndefined<string | Int | Float | boolean | null | undefined>;
export type AllowedAttributeType = string | Int | Float | boolean | null;

export type AttributeValue<V extends AllowedAttributeType = AllowedAttributeType, IsInitialized extends boolean = false> = IsInitialized extends true ? V : V | undefined;

export enum DeviceAttributeModifier
{
Expand All @@ -12,13 +12,16 @@ export enum DeviceAttributeModifier
writeOnly = 'wo',
}

export const isValidAttributeValue = <T extends AttributeValue>(
attribute: DeviceAttribute<T> | undefined,
export const isValidAttributeValue = <V extends AllowedAttributeType, IsInitialized extends boolean = boolean>(
attribute: DeviceAttribute<V, IsInitialized> | undefined,
value: unknown,
): value is NotUndefined<T> => attribute?.isValidValue(value) ?? false;
): value is V => attribute?.isValidValue(value) ?? false;

@Exclude()
export default abstract class DeviceAttribute<T extends AttributeValue = AttributeValue>
export default abstract class DeviceAttribute<
V extends AllowedAttributeType = AllowedAttributeType,
IsInitialized extends boolean = false,
>
{
@Expose({ name: 'name' })
private readonly _name: string;
Expand All @@ -30,9 +33,9 @@ export default abstract class DeviceAttribute<T extends AttributeValue = Attribu
private readonly _modifier: DeviceAttributeModifier;

@Expose({ name: 'value' })
private _value: T;
private _value: AttributeValue<V, IsInitialized>;

public constructor(name: string, label: string | undefined, modifier: DeviceAttributeModifier, initialValue: T) {
public constructor(name: string, label: string | undefined, modifier: DeviceAttributeModifier, initialValue: AttributeValue<V, IsInitialized>) {
this._name = name;
this._label = label;
this._modifier = modifier;
Expand All @@ -58,15 +61,15 @@ export default abstract class DeviceAttribute<T extends AttributeValue = Attribu
/**
* @returns the current value or undefined if it has never been set or read from the device
*/
public get value(): T {
public get value(): AttributeValue<V, IsInitialized> {
return this._value;
}

public set value(value: T) {
public set value(value: AttributeValue<V, IsInitialized>) {
this._value = value;
}

public hasValue(): this is { value: T } {
public hasValue(): this is { value: V } {
return this._value !== undefined;
}

Expand All @@ -76,7 +79,7 @@ export default abstract class DeviceAttribute<T extends AttributeValue = Attribu
throw new Error(`Not implemented`);
}

public abstract fromString(value: string): T;
public abstract fromString(value: string): V;

public abstract isValidValue(value: unknown): value is NotUndefined<T>;
public abstract isValidValue(value: unknown): value is V;
}
22 changes: 11 additions & 11 deletions src/device/attribute/floatDeviceAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import type { DeviceAttributeModifier, NotJustUndefined } from './deviceAttribute.js';
import type { AttributeValue, DeviceAttributeModifier } from './deviceAttribute.js';
import { Float } from '../../util/numbers.js';
import NumberDeviceAttribute from './numberDeviceAttribute.js';

type FloatDeviceAttributeValue = NotJustUndefined<Float | undefined>;
export type InitializedFloatGenericDeviceAttribute = FloatDeviceAttribute<true>;

export type InitializedFloatGenericDeviceAttribute = FloatDeviceAttribute<Float>;

export default class FloatDeviceAttribute<T extends FloatDeviceAttributeValue = FloatDeviceAttributeValue> extends NumberDeviceAttribute<T>
export default class FloatDeviceAttribute<IsInitialized extends boolean = false> extends NumberDeviceAttribute<Float, IsInitialized>
{
public constructor(
name: string,
label: string | undefined,
modifier: DeviceAttributeModifier,
uom: string | undefined,
initialValue: T,
initialValue: AttributeValue<Float, IsInitialized>,
) {
super(name, label, modifier, uom, initialValue);
}
Expand All @@ -25,7 +23,7 @@ export default class FloatDeviceAttribute<T extends FloatDeviceAttributeValue =
uom: string | undefined,
initialValue: Float,
): InitializedFloatGenericDeviceAttribute {
return new FloatDeviceAttribute<Float>(name, label, modifier, uom, initialValue);
return new FloatDeviceAttribute<true>(name, label, modifier, uom, initialValue);
}

public static create(
Expand All @@ -37,16 +35,18 @@ export default class FloatDeviceAttribute<T extends FloatDeviceAttributeValue =
return new FloatDeviceAttribute(name, label, modifier, uom, undefined);
}

public fromString(value: string): T {
public fromString(value: string): Float {
const num = parseFloat(value);

if (isNaN(num)) {
throw new Error(`Could not convert '${value}' to a valid value for ${this.constructor.name}`);
}

// TODO https://github.com/SlvCtrlPlus/slvctrlplus-server/issues/107
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-type-assertion
return Float.from(num) as T;
return Float.from(num);
}

public override isValidValue(value: unknown): value is Float {
return typeof value === 'number' && Number.isFinite(value);
}

public override getType(): string {
Expand Down
19 changes: 10 additions & 9 deletions src/device/attribute/intDeviceAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { DeviceAttributeModifier, NotJustUndefined } from './deviceAttribute.js';
import type { DeviceAttributeModifier } from './deviceAttribute.js';
import { Int } from '../../util/numbers.js';
import NumberDeviceAttribute from './numberDeviceAttribute.js';

export type IntAttributeValue = NotJustUndefined<Int | undefined>;
export type InitializedIntGenericDeviceAttribute = IntDeviceAttribute<Int>;
export type InitializedIntGenericDeviceAttribute = IntDeviceAttribute<true>;

export default class IntDeviceAttribute<T extends IntAttributeValue = IntAttributeValue> extends NumberDeviceAttribute<T>
export default class IntDeviceAttribute<IsInitialized extends boolean = false> extends NumberDeviceAttribute<Int, IsInitialized>
{
public static createInitialized(
name: string,
Expand All @@ -14,7 +13,7 @@ export default class IntDeviceAttribute<T extends IntAttributeValue = IntAttribu
uom: string | undefined,
initialValue: Int,
): InitializedIntGenericDeviceAttribute {
return new IntDeviceAttribute<Int>(name, label, modifier, uom, initialValue);
return new IntDeviceAttribute<true>(name, label, modifier, uom, initialValue);
}

public static create(
Expand All @@ -26,16 +25,18 @@ export default class IntDeviceAttribute<T extends IntAttributeValue = IntAttribu
return new IntDeviceAttribute(name, label, modifier, uom, undefined);
}

public fromString(value: string): T {
public fromString(value: string): Int {
const num = parseInt(value, 10);

if (isNaN(num)) {
throw new Error(`Could not convert '${value}' to a valid value for ${this.constructor.name}`);
}

// TODO https://github.com/SlvCtrlPlus/slvctrlplus-server/issues/107
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-type-assertion
return Int.from(num) as T;
return Int.from(num);
}

public override isValidValue(value: unknown): value is Int {
return typeof value === 'number' && Number.isInteger(value);
}

public override getType(): string {
Expand Down
30 changes: 20 additions & 10 deletions src/device/attribute/intRangeDeviceAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Expose } from 'class-transformer';
import type { IntAttributeValue } from './intDeviceAttribute.js';
import { Int } from '../../util/numbers.js';
import type { DeviceAttributeModifier } from './deviceAttribute.js';
import type { AttributeValue, DeviceAttributeModifier } from './deviceAttribute.js';
import NumberDeviceAttribute from './numberDeviceAttribute.js';

export type InitializedIntRangeDeviceAttribute = IntRangeDeviceAttribute<Int>;
export type InitializedIntRangeDeviceAttribute = IntRangeDeviceAttribute<true>;

export default class IntRangeDeviceAttribute<T extends IntAttributeValue = IntAttributeValue> extends NumberDeviceAttribute<T>
export default class IntRangeDeviceAttribute<IsInitialized extends boolean = false> extends NumberDeviceAttribute<Int, IsInitialized>
{
@Expose({ name: 'min' })
private _min: Int;
Expand All @@ -17,7 +16,16 @@ export default class IntRangeDeviceAttribute<T extends IntAttributeValue = IntAt
@Expose({ name: 'incrementStep' })
private readonly _incrementStep: Int = Int.from(1);

public constructor(name: string, label: string | undefined, modifier: DeviceAttributeModifier, uom: string | undefined, min: Int, max: Int, incrementStep: Int, initialValue: T) {
public constructor(
name: string,
label: string | undefined,
modifier: DeviceAttributeModifier,
uom: string | undefined,
min: Int,
max: Int,
incrementStep: Int,
initialValue: AttributeValue<Int, IsInitialized>,
) {
super(name, label, modifier, uom, initialValue);
this._min = min;
this._max = max;
Expand All @@ -34,7 +42,7 @@ export default class IntRangeDeviceAttribute<T extends IntAttributeValue = IntAt
incrementStep: Int,
initialValue: Int,
): InitializedIntRangeDeviceAttribute {
return new IntRangeDeviceAttribute<Int>(name, label, modifier, uom, min, max, incrementStep, initialValue);
return new IntRangeDeviceAttribute<true>(name, label, modifier, uom, min, max, incrementStep, initialValue);
}

public static create(
Expand Down Expand Up @@ -69,16 +77,18 @@ export default class IntRangeDeviceAttribute<T extends IntAttributeValue = IntAt
return this._incrementStep;
}

public fromString(value: string): T {
public fromString(value: string): Int {
const res = parseInt(value, 10);

if (isNaN(res)) {
throw new Error(`Could not convert '${value}' to a valid value for ${this.constructor.name}`);
}

// TODO https://github.com/SlvCtrlPlus/slvctrlplus-server/issues/107
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-type-assertion
return res as T;
return Int.from(res);
}

public override isValidValue(value: unknown): value is Int {
return typeof value === 'number' && Number.isInteger(value);
}

public override getType(): string {
Expand Down
24 changes: 12 additions & 12 deletions src/device/attribute/listDeviceAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Expose } from 'class-transformer';
import type { DeviceAttributeModifier, NotUndefined } from './deviceAttribute.js';
import type { AttributeValue, DeviceAttributeModifier } from './deviceAttribute.js';
import DeviceAttribute from './deviceAttribute.js';
import type { Int } from '../../util/numbers.js';

Expand All @@ -9,15 +9,15 @@ export type ListDeviceAttributeItem = string | Int;
export type InitializedListDeviceAttribute<
IKey extends ListDeviceAttributeItem,
IValue extends ListDeviceAttributeItem,
> = ListDeviceAttribute<IKey, IValue, IKey>;
> = ListDeviceAttribute<IKey, IValue, true>;

export type ListDeviceAttributeOptions<IKey, IValue> = ListDeviceAttributeOption<IKey, IValue>[];

export default class ListDeviceAttribute<
IKey extends ListDeviceAttributeItem,
IValue extends ListDeviceAttributeItem,
V extends IKey | undefined = IKey | undefined,
> extends DeviceAttribute<V>
IsInitialized extends boolean = false,
> extends DeviceAttribute<IKey, IsInitialized>
{
@Expose({ name: 'values' })
private _values: ListDeviceAttributeOptions<IKey, IValue>;
Expand All @@ -27,7 +27,7 @@ export default class ListDeviceAttribute<
label: string | undefined,
modifier: DeviceAttributeModifier,
values: ListDeviceAttributeOptions<IKey, IValue>,
initialValue: V,
initialValue: AttributeValue<IKey, IsInitialized>,
) {
super(name, label, modifier, initialValue);

Expand All @@ -41,7 +41,7 @@ export default class ListDeviceAttribute<
values: ListDeviceAttributeOptions<IKey, IValue>,
initialValue: IKey,
): InitializedListDeviceAttribute<IKey, IValue> {
return new ListDeviceAttribute<IKey, IValue, IKey>(
return new ListDeviceAttribute<IKey, IValue, true>(
name, label, modifier, values, initialValue,
);
}
Expand All @@ -57,18 +57,18 @@ export default class ListDeviceAttribute<
);
}

public fromString(value: string): V {
public fromString(value: string): IKey {
if (this._values.length === 0 || typeof this._values[0]?.key === 'string') {
// TODO https://github.com/SlvCtrlPlus/slvctrlplus-server/issues/107
// The value kind (IKey) is chosen by the caller per instance, so TypeScript can't
// prove `value`/the parsed number is an IKey here - see issue #107 for details.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-type-assertion
return value as V;
return value as IKey;
}

const parsedInt = parseInt(value, 10);

// TODO https://github.com/SlvCtrlPlus/slvctrlplus-server/issues/107
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-type-assertion
return (isNaN(parsedInt) ? value : parsedInt) as V;
return (isNaN(parsedInt) ? value : parsedInt) as IKey;
}

public get values(): ListDeviceAttributeOptions<IKey, IValue> {
Expand All @@ -79,7 +79,7 @@ export default class ListDeviceAttribute<
this._values = value;
}

public isValidValue(value: unknown): value is NotUndefined<V> {
public isValidValue(value: unknown): value is IKey {
if (typeof value === 'string' || typeof value === 'number') {
return -1 !== this._values.findIndex(entry => entry.key === value);
}
Expand Down
15 changes: 7 additions & 8 deletions src/device/attribute/numberDeviceAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { DeviceAttributeModifier, NotJustUndefined, NotUndefined } from './deviceAttribute.js';
import type { AttributeValue, DeviceAttributeModifier } from './deviceAttribute.js';
import DeviceAttribute from './deviceAttribute.js';
import { Expose } from 'class-transformer';
import type { Float, Int } from '../../util/numbers.js';

export type NumberAttributeValue = NotJustUndefined<Int | Float | undefined>;
export type NumberAttributeValue = Int | Float;

export default abstract class NumberDeviceAttribute<T extends NumberAttributeValue = NumberAttributeValue> extends DeviceAttribute<T>
export default abstract class NumberDeviceAttribute<
V extends NumberAttributeValue = NumberAttributeValue,
IsInitialized extends boolean = false,
> extends DeviceAttribute<V, IsInitialized>
{
@Expose({ name: 'uom' })
private readonly _uom: string | undefined;
Expand All @@ -15,7 +18,7 @@ export default abstract class NumberDeviceAttribute<T extends NumberAttributeVal
label: string | undefined,
modifier: DeviceAttributeModifier,
uom: string | undefined,
initialValue: T,
initialValue: AttributeValue<V, IsInitialized>,
) {
super(name, label, modifier, initialValue);
this._uom = uom;
Expand All @@ -24,8 +27,4 @@ export default abstract class NumberDeviceAttribute<T extends NumberAttributeVal
public get uom(): string | undefined {
return this._uom;
}

public override isValidValue(value: unknown): value is NotUndefined<T> {
return typeof value === 'number';
}
}
Loading
Loading