Skip to content

Commit

Permalink
feat: added accessory and config UI
Browse files Browse the repository at this point in the history
  • Loading branch information
kovapatrik committed Apr 11, 2024
1 parent 41fe1ca commit e102809
Show file tree
Hide file tree
Showing 20 changed files with 727 additions and 112 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"sourceType": "module"
},
"ignorePatterns": [
"dist"
"dist",
"homebridge-ui"
],
"rules": {
"quotes": ["warn", "single"],
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Publish to npm

on:
push:
branches:
- main

jobs:
publish:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, 'skip ci')"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm run lint
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
2 changes: 1 addition & 1 deletion config.schema.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"pluginAlias": "BlueAirPlatform",
"pluginAlias": "blueair-purifier",
"pluginType": "platform",
"singular": true,
"schema": {
Expand Down
6 changes: 6 additions & 0 deletions homebridge-ui/public/css/bootstrap.min.css

Large diffs are not rendered by default.

263 changes: 263 additions & 0 deletions homebridge-ui/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
<link href="css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous" />
<script src="js/bootstrap.min.js" crossorigin="anonymous"></script>

<div id="main" data-bs-theme="dark">
<div class="card">

<div class="card-header" id="uiButtons">
<h5 class="mb-0 text-center">
<button class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#discoverDevices"
aria-expanded="false" aria-controls="discoverDevices" style="border: 0;">
Discover devices
</button>
</h5>
</div>

<div class="collapse" id="discoverDevices">
<div class="card-body">
<form>
<div class="mb-3" id="login">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password">
</div>
<div class="mb-3">
<label for="region" class="form-label">Region</label>
<select class="form-select" id="region">
<option value="Default (all other regions)">Default (all other regions)</option>
<option value="Australia">Australia</option>
<option value="China">China</option>
<option value="Russia">Russia</option>
<option value="USA">USA</option>
</div>
<button class="btn btn-primary btn-login" type="submit" id="discoverBtn" style="border: 0;">
Discover Devices on account
</button>
</form>
<div class="text-center device-table" style="display: none" id="discoverTableWrapper">
<table class="table table-sm" id="discoverTable">
<thead>
<tr>
<th scope="col">Id</th>
<th scope="col">Mac</th>
<th scope="col">Account UUID</th>
<th scope="col">Add&nbsp;/&nbsp;Update</th>
</tr>
</thead>
<tbody>
<tr>
<td>No devices found!</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

</div>

<style>

.hide {
display: none;
}

.disabled {
pointer-events: none;
opacity: 0.5;
}
</style>


<script>

(async () => {
/*********************************************************************
* Initialize Javascript supporting code
*/
homebridge.showSpinner();

const { defaultConfig, defaultDeviceConfig } = await homebridge.request('/getDefaults');
const pluginConfig = await homebridge.getPluginConfig();
const configSchema = await homebridge.getPluginConfigSchema();

if (!pluginConfig.length) {
pluginConfig.push({});
}
let configuration = pluginConfig[0];

// Helper funcion to contol debug messages
function debugLog(s) {
if (configuration.uiDebug) {
console.debug(s);
}
}

debugLog(`Plugin Config:\n${JSON.stringify(configuration, null, 2)}`);
configuration = await homebridge.request('/mergeToDefault', { config: configuration });

/*********************************************************************
* showToast event listener
* Provides information from server-side to the end user.
*/
homebridge.addEventListener('showToast', (event) => {
debugLog(`showToast Received: ${JSON.stringify(event.data)}`);
if (event.data.success) {
homebridge.toast.success(event.data.msg);
} else {
homebridge.toast.error(event.data.msg);
}
});

/*********************************************************************
* filterOutDefaults
* returns object for config.json that has default values removed
*/
function filterOutDefaults(object, defaults) {

function deleteEmptyObjects(object) {
for (const [k, v] of Object.entries(object)) {
if (!v || typeof v !== 'object' || v === null) {
continue;
}
deleteEmptyObjects(v);
if (Object.keys(v).length === 0) {
delete object[k];
}
}
return object;
}

let newObject = {};
for (const [k, v] of Object.entries(object)) {
if (k === 'devices') {
newObject[k] = v.map((device) => {
return filterOutDefaults(device, defaultDeviceConfig);
});
} else if (typeof v === 'object' && v !== null) {
newObject[k] = filterOutDefaults(v, defaults[k]);
} else if (typeof defaults === 'object' && k in defaults && v === defaults[k]) {
continue;
} else {
newObject[k] = v;
}
}
return deleteEmptyObjects(newObject);
}

/*********************************************************************
* createForm
* Update the plugin config GUI. Does not save to config.json
*/
function createForm(configSchema, configuration) {
const configForm = homebridge.createForm(configSchema, configuration);
configForm.onChange(async (changes) => {
changes = filterOutDefaults(changes, defaultConfig);
debugLog(`[createForm] Config changes:\n${JSON.stringify(changes, null, 2)}`);
await homebridge.updatePluginConfig([changes]);
});
}

createForm(configSchema, configuration);
homebridge.hideSpinner();

/*********************************************************************
* Discover button clicked....
*/
document.getElementById('discoverBtn').addEventListener('click', async (e) => {
e.preventDefault();

const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const region = document.getElementById('region').value;

homebridge.showSpinner();

console.info(`Request login...`);
try {
const devices = await homebridge.request('/disover', { username, password, region });
debugLog(`Discovered devices:\n${JSON.stringify(devices, null, 2)}`);
const table = document.getElementById('discoverTable').getElementsByTagName('tbody')[0];
table.innerHTML = '';

// Merge current config over to the server so discover has latest device config...
let currentConfig = await homebridge.getPluginConfig();
currentConfig = currentConfig.length > 0 ? currentConfig[0] : {};
configuration = await homebridge.request('/mergeToDefault', { config: currentConfig });

currentConfig.devices = currentConfig.devices || [];
if (devices) {

const accountUuid = devices[0].name;

configuration.username = username;
configuration.password = password;
configuration.region = region;
configuration.accountUuid = accountUuid;

const initialStates = await homebridge.request('/getInitialDeviceStates', { accountUuid, uuids: devices.map((d) => d.uuid) });

devices.forEach((device) => {
const state = initialStates.find((s) => s.id === device.uuid);
const tr = table.insertRow();
const td = tr.insertCell();
td.appendChild(document.createTextNode(device.uuid));
td.setAttribute('scope', 'row');

tr.insertCell().appendChild(document.createTextNode(device.mac));
tr.insertCell().appendChild(document.createTextNode(device.name));

const addCell = tr.insertCell();
if (currentConfig.devices.find((d) => d.id === device.uuid)) {
addCell.appendChild(document.createTextNode('Already added'));
} else {
// No record for this device, add button to add it.
const button = document.createElement('button');
button.innerText = 'Add';
button.className = 'btn btn-secondary btn-sm';
button.addEventListener('click', async () => {
// Add button clicked...
const newDevice = {
id: device.uuid,
name: state.name
};

configuration.devices.push({
...defaultDeviceConfig,
...newDevice
});
debugLog(`Adding new device:\n${JSON.stringify({ ...defaultDeviceConfig, ...newDevice }, null, 2)}`);
// refresh view
createForm(configSchema, configuration);

addCell.removeChild(button);
addCell.appendChild(document.createTextNode('Added'));

homebridge.toast.success('Device added');
});
addCell.appendChild(button);
}
});
} else {
table.innerHTML += `<tr><td>No devices found!</td></tr>`;
}
document.getElementById('discoverTableWrapper').style.display = 'block';

homebridge.hideSpinner();

} catch (e) {
homebridge.toast.error(e.message);
homebridge.hideSpinner();
return;

}

});
})();

</script>
7 changes: 7 additions & 0 deletions homebridge-ui/public/js/bootstrap.min.js

Large diffs are not rendered by default.

Loading

0 comments on commit e102809

Please sign in to comment.