Skip to content

Commit

Permalink
Added preferred phase management
Browse files Browse the repository at this point in the history
  • Loading branch information
davbauer committed Oct 20, 2023
1 parent 1845dbb commit a535492
Show file tree
Hide file tree
Showing 13 changed files with 188 additions and 27 deletions.
2 changes: 2 additions & 0 deletions backend/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import chargeRoutes from './routes/chargeRoutes.js';
import configRoutes from './routes/configRoutes.js';
import configEnabledRoutes from './routes/configEnabledRoutes.js';
import configEnabledPowergridRoutes from './routes/configEnabledPowergridRoutes.js';
import configPreferredPhase from './routes/configPreferredPhase.js'
import livedataRoutes from './routes/livedataRoutes.js';
import appInfoRoutes from './routes/appInfoRoutes.js';
import AppInfo from './classes/AppInfo.js';
Expand All @@ -36,6 +37,7 @@ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
app.use(express.static('./svelte-build'));
app.use('/', chargeRoutes);
app.use('/', configRoutes);
app.use('/', configPreferredPhase)
app.use('/', configEnabledRoutes);
app.use('/', configEnabledPowergridRoutes);
app.use('/', livedataRoutes);
Expand Down
5 changes: 5 additions & 0 deletions backend/classes/WebSocketManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as ws from 'ws';
import LiveData from './LiveData.js';
import infoLog from '../functions/infoLog.js';
import InterfaceConfig from '../models/InterfaceConfig.js';

export default class WebSocketManager {
private static wss: ws.WebSocketServer | null = null;
Expand Down Expand Up @@ -50,6 +51,10 @@ export default class WebSocketManager {
this.sendEvent('backendTerminalUpdate', { type, msg, ts });
}

public static sendEventPreferredPhase(state: Pick<InterfaceConfig, "PreferredPhase">) {
this.sendEvent('preferredPhaseUpdate', { state });
}

public static sendEvent(eventType: string, data: any): void {
// Need to fix this issue later, probably via websocket connection Id, dont dissallow too many connections issue!
// 20 Oct. 2023 - David
Expand Down
32 changes: 21 additions & 11 deletions backend/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import ConfigFile from './classes/ConfigFile.js';
import InterfaceConfig from './models/InterfaceConfig.js';
import LiveDataInterface from './models/InterfaceLiveData.js';
import infoLog from './functions/infoLog.js';
import errorLog from './functions/errorLog.js';

const CAR_NOT_CHARGING = 2;
const CAR_WAIT = 3;

export default async function (): Promise<void> {
console.log('\n---------------------------------------------------------------');
infoLog('\n---------------------------------------------------------------');
infoLog('LOOP (' + ConfigFile.read().CheckSeconds + 's) --------------------|');

LiveData.data = LiveData.defaultData;
Expand Down Expand Up @@ -134,24 +135,33 @@ export default async function (): Promise<void> {
infoLog('Done');
}

function findClosestValue(key: number, mappingArray: any[]): any {
// if length is zero just return a preset value indicating 'None'
if (mappingArray.length === 0) {
function findClosestValue(availablePower: number, mappingArray: any[]): any {
const preferredPhase = ConfigFile.read().PreferredPhase;

// Filter the mappings based on the preferredPhase first.
const filteredMappings = mappingArray.filter(item => {
return (preferredPhase === 0) || // 0 for auto (accept all)
(preferredPhase === 1 && item.onePhase) || // 1 for phase 1
(preferredPhase === 2 && !item.onePhase); // 2 for phase 3
});

// If there are no mappings left after filtering, return a default response.
if (filteredMappings.length === 0) {
errorLog('No mappings available for the preferred phase.');
type MappingItemType = InterfaceConfig['Mapping'][0];
const response: MappingItemType = { value: 0, amp: 0, onePhase: false };
return response;
}

return mappingArray.reduce((prev, curr) => {
// If the current value is closer or the same distance to the key than the previous
// and is not more than the key, then consider it as the closest value.
if (Math.abs(curr.value - key) <= Math.abs(prev.value - key) && curr.value <= key) {
return curr;
}
return prev;
// Find the mapping that is closest to availablePower.
const optimalMapping = filteredMappings.reduce((prev, curr) => {
return (Math.abs(curr.value - availablePower) < Math.abs(prev.value - availablePower)) ? curr : prev;
});

return optimalMapping;
}


function calculateChargeSettings(config: InterfaceConfig) {
const availablePower = LiveData.data.Inverter.Export + LiveData.data.Charger.Consumption;

Expand Down
73 changes: 73 additions & 0 deletions backend/routes/configPreferredPhase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import express from 'express';
import ConfigFile from '../classes/ConfigFile.js';
import errorLog from '../functions/errorLog.js';
import WebSocketManager from '../classes/WebSocketManager.js';

const r = express.Router();

/**
* @swagger
* /preferredPhase:
* post:
* summary: Update the use preferredPhase state
* tags:
* - Configuration
* description: Post data based on InterfaceConfig model.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: 'object'
* properties:
* state:
* type: 'string'
* description: The new preferredPhase state.
* example: true
* responses:
* 200:
* description: Success
* content:
* application/json:
* schema:
* type: 'object'
* properties:
* msg:
* type: 'string'
* example: 'success'
* 500:
* description: Error
* content:
* application/json:
* schema:
* type: 'object'
* properties:
* msg:
* type: 'string'
* example: 'Error writing to config file'
*/
r.post('/preferredPhase', async (req, res) => {
const stateData = req.body.state;
if (stateData !== 0 && stateData !== 1 && stateData !== 2) {
res.status(400).json({
msg: 'Bad Request: state must be 0, 1, or 2'
});
return;
}
const configData = ConfigFile.read();
configData.PreferredPhase = stateData;
const success = ConfigFile.write(configData);

// It's better to send events after ensuring that data is written successfully.
if (success) {
WebSocketManager.sendEventPreferredPhase(stateData); // Adjusted position
res.status(200).json({ msg: 'success' });
} else {
errorLog('Error writing preferredPhase state to config file.');
res.status(500).json({
msg: 'Error writing to config file'
});
}
});

export default r;
File renamed without changes.
8 changes: 8 additions & 0 deletions src/lib/api/services/ServiceConfigPreferredPhase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type ResBase from '../models/ResBase';
import ApiBase from './ApiBase';

export default class extends ApiBase {
static async postEnabled(state: 0 | 1 | 2): Promise<ResBase> {
return this.post<ResBase>('preferredPhase', { state });
}
}
8 changes: 8 additions & 0 deletions src/lib/api/services/ServiceWebsocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { get } from 'svelte/store';
import { newErrorToast, newInfoToast } from '../Utilities/UtilStoreToast';
import type LiveData from '../models/LiveData';
import type BackendLogs from '../models/BackendLogs';
import type Config from '../models/Config';

export default class {
private static RETRY_DELAY = 1000; // Start with 1 second
Expand Down Expand Up @@ -36,6 +37,13 @@ export default class {
UsePowergrid: message.data.state as boolean
});
break;
case 'preferredPhaseUpdate':
newInfoToast('Received ' + message.event);
config.set({
...get(config),
PreferredPhase: (message.data.state as 0 | 1 | 2) ?? 0
});
break;
case 'liveDataUpdate':
newInfoToast('Received ' + message.event);
liveData.set(message.data as LiveData);
Expand Down
29 changes: 17 additions & 12 deletions src/lib/components/SectionAmpereMapping.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,28 @@
}
$: getClassForRow = (row: Config['Mapping'][0]) => {
const isPhaseMatch =
$liveData.Charger.PhaseMode === 0 ||
($liveData.Charger.PhaseMode === 1 && row.onePhase) ||
($liveData.Charger.PhaseMode === 2 && !row.onePhase);
const isCurrentPhaseMatch =
$liveData.Charger.PhaseMode === 0 || // Match any if PhaseMode is 0
($liveData.Charger.PhaseMode === 1 && row.onePhase) || // Match only phase 1
($liveData.Charger.PhaseMode === 2 && !row.onePhase); // Match only phase 3
const isCalcMatch =
row.amp === $liveData.Charger.AmpCalc && $liveData.Charger.AmpCalc !== $liveData.Charger.Amp;
// Determine if the row matches the calculated phase settings.
const isCalcPhaseMatch =
$liveData.Charger.PhaseModeCalc === 0 || // Match any if PhaseModeCalc is 0
($liveData.Charger.PhaseModeCalc === 1 && row.onePhase) || // Match only phase 1
($liveData.Charger.PhaseModeCalc === 2 && !row.onePhase); // Match only phase 3
const isMatch = row.amp === $liveData.Charger.Amp;
// Determine if the row matches the calculated Amp settings.
const isCalcAmpMatch = row.amp === $liveData.Charger.AmpCalc;
const isOutOfRange = row.value < $config.MinimumWatts || row.value > $config.MaximumWatts;
// Determine if the row matches the current Amp settings.
const isCurrentAmpMatch = row.amp === $liveData.Charger.Amp;
return {
isPhaseAndCalcMatch: isPhaseMatch && isCalcMatch,
isPhaseAndAmpMatch: isPhaseMatch && isMatch,
isOutOfRange
// The row matches both the calculated phase and amp settings.
isPhaseAndCalcMatch: isCalcPhaseMatch && isCalcAmpMatch,
// The row matches both the current phase and amp settings.
isPhaseAndAmpMatch: isCurrentPhaseMatch && isCurrentAmpMatch
};
};
</script>
Expand All @@ -81,7 +87,6 @@
class:bg-neutral-focus={getClassForRow(row).isPhaseAndCalcMatch}
class:border-l-2={getClassForRow(row).isPhaseAndCalcMatch}
class:bg-secondary={getClassForRow(row).isPhaseAndAmpMatch}
class:opacity-40={getClassForRow(row).isOutOfRange}
>
<td class="w-1/3 text-left">
<label class="swap swap-flip flex justify-center">
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/SectionEnabledPowergridSwitch.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script lang="ts">
import ServiceEnabledPowergrid from '$lib/api/services/ServiceEnabledPowergrid';
import ServiceConfigEnabledPowergrid from '$lib/api/services/ServiceConfigEnabledPowergrid';
import { newSuccessToast, newErrorToast } from '$lib/api/Utilities/UtilStoreToast';
import { config } from '$lib/store';
async function onEnabledPowergridChange(event: any) {
await ServiceEnabledPowergrid.postEnabledPowergrid(event.target.checked)
await ServiceConfigEnabledPowergrid.postEnabledPowergrid(event.target.checked)
.then(() => {
newSuccessToast('Powergrid state changed: ' + event.target.checked);
})
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/SectionEnabledSwitch.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script lang="ts">
import ServiceEnabled from '$lib/api/services/ServiceEnabled';
import ServiceConfigEnabled from '$lib/api/services/ServiceConfigEnabled';
import { newSuccessToast, newErrorToast } from '$lib/api/Utilities/UtilStoreToast';
import { config } from '$lib/store';
async function onEnabledChange(event: any) {
await ServiceEnabled.postEnabled(event.target.checked)
await ServiceConfigEnabled.postEnabled(event.target.checked)
.then(() => {
newSuccessToast('Enabled state changed: ' + event.target.checked);
})
Expand Down
47 changes: 47 additions & 0 deletions src/lib/components/SectionPhaseSwitch.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import { newErrorToast, newSuccessToast } from '$lib/api/Utilities/UtilStoreToast';
import ServiceConfigPreferredPhase from '$lib/api/services/ServiceConfigPreferredPhase';
import { config } from '$lib/store';
async function onPreferredPhaseChange() {
await ServiceConfigPreferredPhase.postEnabled($config.PreferredPhase)
.then(() => {
newSuccessToast('Preferred Phase changed: ' + $config.PreferredPhase);
})
.catch((err) => {
newErrorToast('Preferred Phase change error: ' + err.message);
});
}
</script>

<div class="mt-3 p-3 bg-neutral rounded-md">
<p class="text-2xl underline">PreferredPhase</p>
<div class="flex justify-center">
<div class="join text-center" on:change={onPreferredPhaseChange}>
<input
type="radio"
bind:group={$config.PreferredPhase}
value={0}
name="options"
aria-label="Auto"
class="join-item btn"
/>
<input
type="radio"
bind:group={$config.PreferredPhase}
value={1}
name="options"
aria-label="Phase 1"
class="join-item btn"
/>
<input
type="radio"
bind:group={$config.PreferredPhase}
value={2}
name="options"
aria-label="Phase 2"
class="join-item btn"
/>
</div>
</div>
</div>
3 changes: 3 additions & 0 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import SectionSettings from '$lib/components/SectionSettings.svelte';
import ServiceWebsocket from '$lib/api/services/ServiceWebsocket';
import SectionEnabledPowergridSwitch from '$lib/components/SectionEnabledPowergridSwitch.svelte';
import SectionPhaseSwitch from '$lib/components/SectionPhaseSwitch.svelte';
onMount(async () => {
ServiceWebsocket.initSocket();
Expand All @@ -18,6 +19,8 @@

<SectionEnabledPowergridSwitch />

<SectionPhaseSwitch />

<SectionChargeControl />

<SectionLiveData />
Expand Down

0 comments on commit a535492

Please sign in to comment.