diff --git a/app/components/DeviceView.tsx b/app/components/DeviceView.tsx
index 0e4658f..2625ba4 100644
--- a/app/components/DeviceView.tsx
+++ b/app/components/DeviceView.tsx
@@ -10,31 +10,27 @@ import "./DeviceView.scss";
const ConnectionState = observer(({ connectionState, className }:
{ connectionState: ConState, className?: string }) => {
- const connected = connectionState.isConnected;
- const classes = classNames({
- connectionState: true,
- connected: connected === true,
- disconnected: connected === false,
- unknown: connected === null,
- }, className);
+ const connected = connectionState.isDeviceConnected;
let connectionText: string;
let iconName: SemanticICONS = "unlinkify";
+ let clazzName: string = "disconnected";
if (connected) {
connectionText = "Connected";
iconName = "linkify";
- } else if (connected === null) {
- connectionText = "Unknown";
- iconName = "question";
+ clazzName = "connected";
+ } else if (connected === false) {
+ connectionText = "Device Disconnected";
} else if (connectionState.noPermission) {
connectionText = "No permission for this device";
iconName = "ban";
- } else if (connectionState.serverToBroker) {
- connectionText = "Device Disconnected";
- } else if (connectionState.clientToServer) {
- connectionText = "Broker Disconnected";
+ } else if (connectionState.clientToServer === false) {
+ connectionText = "Disconnected from server";
} else {
- connectionText = "Server Disconnected";
+ connectionText = "Unknown";
+ iconName = "question";
+ clazzName = "unknown";
}
+ const classes = classNames("connectionState", clazzName, className);
return (
diff --git a/app/sprinklersRpc/websocketClient.ts b/app/sprinklersRpc/WebSocketRpcClient.ts
similarity index 99%
rename from app/sprinklersRpc/websocketClient.ts
rename to app/sprinklersRpc/WebSocketRpcClient.ts
index f005554..cd7d209 100644
--- a/app/sprinklersRpc/websocketClient.ts
+++ b/app/sprinklersRpc/WebSocketRpcClient.ts
@@ -91,7 +91,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
private reconnectTimer: number | null = null;
get connected(): boolean {
- return this.connectionState.isConnected || false;
+ return this.connectionState.isServerConnected || false;
}
constructor(tokenStore: TokenStore, webSocketUrl: string = DEFAULT_URL) {
diff --git a/app/state/AppState.ts b/app/state/AppState.ts
index c65854c..8a3252a 100644
--- a/app/state/AppState.ts
+++ b/app/state/AppState.ts
@@ -2,9 +2,11 @@ import { createBrowserHistory, History } from "history";
import { computed } from "mobx";
import { RouterStore, syncHistoryWithStore } from "mobx-react-router";
-import { WebSocketRpcClient } from "@app/sprinklersRpc/websocketClient";
+import { WebSocketRpcClient } from "@app/sprinklersRpc/WebSocketRpcClient";
import HttpApi from "@app/state/HttpApi";
import { UiStore } from "@app/state/UiStore";
+import ApiError from "@common/ApiError";
+import { ErrorCode } from "@common/ErrorCode";
import log from "@common/logger";
export default class AppState {
@@ -29,9 +31,14 @@ export default class AppState {
try {
await this.httpApi.tokenStore.grantRefresh();
} catch (err) {
- log.warn({ err }, "could not refresh access token. erasing token");
- this.tokenStore.clear();
- this.history.push("/login");
+ if (err instanceof ApiError && err.code === ErrorCode.BadToken) {
+ log.warn({ err }, "refresh is bad for some reason, erasing");
+ this.tokenStore.clear();
+ this.history.push("/login");
+ } else {
+ log.error({ err }, "could not refresh access token");
+ // TODO: some kind of error page?
+ }
}
} else {
this.history.push("/login");
diff --git a/common/sprinklersRpc/ConnectionState.ts b/common/sprinklersRpc/ConnectionState.ts
index 236ce3c..76629b4 100644
--- a/common/sprinklersRpc/ConnectionState.ts
+++ b/common/sprinklersRpc/ConnectionState.ts
@@ -45,18 +45,28 @@ export class ConnectionState {
return false;
}
- @computed get isConnected(): boolean | null {
+ @computed get isDeviceConnected(): boolean | null {
if (this.hasPermission === false) {
return false;
}
+ if (this.serverToBroker === false || this.clientToServer === false) {
+ return null;
+ }
if (this.brokerToDevice != null) {
return this.brokerToDevice;
}
+ return null;
+ }
+
+ @computed get isServerConnected(): boolean | null {
+ if (this.hasPermission === false) {
+ return false;
+ }
if (this.serverToBroker != null) {
return this.serverToBroker;
}
if (this.clientToServer != null) {
- return this.clientToServer;
+ return this.brokerToDevice;
}
return null;
}
diff --git a/common/sprinklersRpc/SprinklersDevice.ts b/common/sprinklersRpc/SprinklersDevice.ts
index bdb64a6..0ef6162 100644
--- a/common/sprinklersRpc/SprinklersDevice.ts
+++ b/common/sprinklersRpc/SprinklersDevice.ts
@@ -12,7 +12,7 @@ export abstract class SprinklersDevice {
@observable sectionRunner: SectionRunner;
@computed get connected(): boolean {
- return this.connectionState.isConnected || false;
+ return this.connectionState.isDeviceConnected || false;
}
sectionConstructor: typeof Section = Section;
diff --git a/common/sprinklersRpc/mqtt/index.ts b/common/sprinklersRpc/mqtt/index.ts
index 7a32d9d..e4d30ce 100644
--- a/common/sprinklersRpc/mqtt/index.ts
+++ b/common/sprinklersRpc/mqtt/index.ts
@@ -7,6 +7,7 @@ import * as s from "@common/sprinklersRpc";
import * as requests from "@common/sprinklersRpc/deviceRequests";
import * as schema from "@common/sprinklersRpc/schema";
import { seralizeRequest } from "@common/sprinklersRpc/schema/requests";
+import { getRandomId } from "@common/utils";
const log = logger.child({ source: "mqtt" });
@@ -14,14 +15,14 @@ interface WithRid {
rid: number;
}
-export class MqttApiClient implements s.SprinklersRPC {
+export class MqttRpcClient implements s.SprinklersRPC {
readonly mqttUri: string;
client!: mqtt.Client;
@observable connectionState: s.ConnectionState = new s.ConnectionState();
devices: Map = new Map();
get connected(): boolean {
- return this.connectionState.isConnected || false;
+ return this.connectionState.isServerConnected || false;
}
constructor(mqttUri: string) {
@@ -30,11 +31,11 @@ export class MqttApiClient implements s.SprinklersRPC {
}
private static newClientId() {
- return "sprinklers3-MqttApiClient-" + Math.round(Math.random() * 1000);
+ return "sprinklers3-MqttApiClient-" + getRandomId();
}
start() {
- const clientId = MqttApiClient.newClientId();
+ const clientId = MqttRpcClient.newClientId();
log.info({ mqttUri: this.mqttUri, clientId }, "connecting to mqtt broker with client id");
this.client = mqtt.connect(this.mqttUri, {
clientId, connectTimeout: 5000, reconnectPeriod: 5000,
@@ -127,14 +128,14 @@ const handler = (test: RegExp) =>
};
class MqttSprinklersDevice extends s.SprinklersDevice {
- readonly apiClient: MqttApiClient;
+ readonly apiClient: MqttRpcClient;
readonly prefix: string;
handlers!: IHandlerEntry[];
private nextRequestId: number = Math.floor(Math.random() * 1000000000);
private responseCallbacks: Map = new Map();
- constructor(apiClient: MqttApiClient, prefix: string) {
+ constructor(apiClient: MqttRpcClient, prefix: string) {
super();
this.sectionConstructor = MqttSection;
this.sectionRunnerConstructor = MqttSectionRunner;
diff --git a/server/models/Database.ts b/server/models/Database.ts
index ec2efb4..d50e30e 100644
--- a/server/models/Database.ts
+++ b/server/models/Database.ts
@@ -1,6 +1,7 @@
import * as r from "rethinkdb";
import logger from "@common/logger";
+import { SprinklersDevice, UserSprinklersDevice } from "./SprinklersDevice";
import { User } from "./User";
export class Database {
@@ -43,6 +44,12 @@ export class Database {
if (tables.indexOf(User.tableName) === -1) {
await User.createTable(this);
}
+ if (tables.indexOf(SprinklersDevice.tableName) === -1) {
+ await SprinklersDevice.createTable(this);
+ }
+ if (tables.indexOf(UserSprinklersDevice.tableName) === -1) {
+ await UserSprinklersDevice.createTable(this);
+ }
const alex = new User(this, {
name: "Alex Mikhalev",
username: "alex",
@@ -53,5 +60,12 @@ export class Database {
const alex2 = await User.loadByUsername(this, "alex");
logger.info("password valid: " + await alex2!.comparePassword("kakashka"));
+
+ const device = new SprinklersDevice(this, {
+ name: "test",
+ });
+ await device.createOrUpdate();
+
+ device.addToUser
}
}
diff --git a/server/models/SprinklersDevice.ts b/server/models/SprinklersDevice.ts
new file mode 100644
index 0000000..2b7f131
--- /dev/null
+++ b/server/models/SprinklersDevice.ts
@@ -0,0 +1,152 @@
+import * as r from "rethinkdb";
+import { createModelSchema, primitive, serialize, update } from "serializr";
+
+import { Database } from "./Database";
+import { User } from "./User";
+
+export interface ISprinklersDevice {
+ id: string | undefined;
+ name: string;
+}
+
+export class SprinklersDevice implements ISprinklersDevice {
+ static readonly tableName = "SprinklersDevices";
+
+ id: string | undefined;
+ name: string = "";
+
+ private db: Database;
+ private _table: r.Table | null = null;
+
+ constructor(db: Database, data?: Partial) {
+ this.db = db;
+ if (data) {
+ update(this, data);
+ }
+ }
+
+ static async createTable(db: Database) {
+ await db.db.tableCreate(SprinklersDevice.tableName).run(db.conn);
+ await db.db.table(SprinklersDevice.tableName).indexCreate("name").run(db.conn);
+ }
+
+ static async loadAll(db: Database): Promise {
+ const cursor = await db.db.table(SprinklersDevice.tableName)
+ .run(db.conn);
+ const users = await cursor.toArray();
+ return users.map((data) => {
+ return new SprinklersDevice(db, data);
+ });
+ }
+
+ static async load(db: Database, id: string): Promise {
+ const data = await db.db.table(SprinklersDevice.tableName)
+ .get(id)
+ .run(db.conn);
+ if (data == null) {
+ return null;
+ }
+ return new SprinklersDevice(db, data);
+ }
+
+ private static getTable(db: Database): r.Table {
+ return db.db.table(this.tableName);
+ }
+
+ private get table() {
+ if (!this._table) {
+ this._table = SprinklersDevice.getTable(this.db);
+ }
+ return this._table;
+ }
+
+ async create() {
+ const data = serialize(this);
+ delete data.id;
+ const result = await this.table
+ .insert(data)
+ .run(this.db.conn);
+ this.id = result.generated_keys[0];
+ }
+
+ async createOrUpdate() {
+ const data = serialize(this);
+ delete data.id;
+ const device = this.table.filter(r.row("name").eq(this.name));
+ const nameDoesNotExist = device.isEmpty();
+ const a: r.WriteResult = await r.branch(nameDoesNotExist,
+ this.table.insert(data) as r.Expression,
+ device.nth(0).update(data) as r.Expression)
+ .run(this.db.conn);
+ if (a.inserted > 0) {
+ this.id = a.generated_keys[0];
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ async addToUser(user: User | number) {
+ const userId = (typeof user === "number") ? user : user.id;
+ const userDevice = new UserSprinklersDevice(this.db, {
+
+ });
+ }
+
+ toJSON(): any {
+ return serialize(this);
+ }
+}
+
+createModelSchema(SprinklersDevice, {
+ id: primitive(),
+ name: primitive(),
+});
+
+export interface IUserSprinklersDevice {
+ id: string | undefined;
+ userId: string;
+ sprinklersDeviceId: string;
+}
+
+export class UserSprinklersDevice implements IUserSprinklersDevice {
+ static readonly tableName = "UserSprinklersDevices";
+
+ id: string | undefined;
+
+ userId: string = "";
+ sprinklersDeviceId: string = "";
+
+ private db: Database;
+ private _table: r.Table | null = null;
+
+ constructor(db: Database) {
+ this.db = db;
+ }
+
+ static async createTable(db: Database) {
+ await db.db.tableCreate(UserSprinklersDevice.tableName).run(db.conn);
+ await db.db.table(UserSprinklersDevice.tableName).indexCreate("userId").run(db.conn);
+ await db.db.table(UserSprinklersDevice.tableName).indexCreate("sprinklersDeviceId").run(db.conn);
+ }
+
+ private static getTable(db: Database): r.Table {
+ return db.db.table(this.tableName);
+ }
+
+ async create() {
+ const data = serialize(this);
+ delete data.id;
+ const result = await this.table
+ .insert(data)
+ .run(this.db.conn);
+ this.id = result.generated_keys[0];
+ }
+
+ private get table() {
+ if (!this._table) {
+ this._table = UserSprinklersDevice.getTable(this.db);
+ }
+ return this._table;
+ }
+}
diff --git a/server/models/User.ts b/server/models/User.ts
index 4c8fe44..41c48ea 100644
--- a/server/models/User.ts
+++ b/server/models/User.ts
@@ -14,7 +14,7 @@ export interface IUser {
const HASH_ROUNDS = 10;
export class User implements IUser {
- static readonly tableName = "users";
+ static readonly tableName = "Users";
id: string | undefined;
username: string = "";
@@ -72,9 +72,10 @@ export class User implements IUser {
async create() {
const data = serialize(this);
delete data.id;
- await this.table
+ const result = await this.table
.insert(data)
.run(this.db.conn);
+ this.id = result.generated_keys[0];
}
async createOrUpdate() {
@@ -86,7 +87,12 @@ export class User implements IUser {
this.table.insert(data) as r.Expression,
user.nth(0).update(data) as r.Expression)
.run(this.db.conn);
- return a.inserted > 0;
+ if (a.inserted > 0) {
+ this.id = a.generated_keys[0];
+ return true;
+ } else {
+ return false;
+ }
}
async setPassword(newPassword: string): Promise {
diff --git a/server/models/index.ts b/server/models/index.ts
new file mode 100644
index 0000000..c3de530
--- /dev/null
+++ b/server/models/index.ts
@@ -0,0 +1,3 @@
+export { User, IUser } from "./User";
+export { SprinklersDevice, ISprinklersDevice, UserSprinklersDevice } from "./SprinklersDevice";
+export { Database } from "./Database";
diff --git a/server/state.ts b/server/state.ts
index 6b5b175..c86e8a0 100644
--- a/server/state.ts
+++ b/server/state.ts
@@ -3,7 +3,7 @@ import * as mqtt from "@common/sprinklersRpc/mqtt";
import { Database } from "./models/Database";
export class ServerState {
- mqttClient: mqtt.MqttApiClient;
+ mqttClient: mqtt.MqttRpcClient;
database: Database;
constructor() {
@@ -11,7 +11,7 @@ export class ServerState {
if (!mqttUrl) {
throw new Error("Must specify a MQTT_URL to connect to");
}
- this.mqttClient = new mqtt.MqttApiClient(mqttUrl);
+ this.mqttClient = new mqtt.MqttRpcClient(mqttUrl);
this.database = new Database();
}