homebridge-eufy-security
Version:
Control Eufy Security from homebridge.
227 lines (194 loc) • 8.34 kB
JavaScript
/**
* Dashboard View — displays device/station grid.
* Main screen after login. Shows all discovered devices as cards.
*/
// eslint-disable-next-line no-unused-vars
const DashboardView = {
_container: null,
async render(container) {
this._container = container;
container.innerHTML = '';
const config = await Config.get();
// Header
const header = document.createElement('div');
header.className = 'eufy-header';
const titleEl = document.createElement('h4');
titleEl.textContent = 'Eufy Security';
header.appendChild(titleEl);
const headerRight = document.createElement('div');
headerRight.className = 'eufy-header__right';
// Cache date label
if (App.state.cacheDate) {
const cacheLabel = document.createElement('span');
cacheLabel.className = 'eufy-cache-date';
const d = new Date(App.state.cacheDate);
cacheLabel.textContent = 'Cached: ' + d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
cacheLabel.title = 'Last fetched: ' + d.toLocaleString();
headerRight.appendChild(cacheLabel);
}
const btnGroup = document.createElement('div');
btnGroup.className = 'd-flex gap-2';
const btnRefresh = this._headerBtn('refresh.svg', 'Re-login / Re-fetch', 'btn-success', () => App.navigate('login'));
const btnDiagnostic = this._headerBtn('bug-report.svg', 'Diagnostics', 'btn-warning', () => App.navigate('diagnostics'));
const btnSettings = this._headerBtn('settings.svg', 'Global Settings', 'btn-danger', () => App.navigate('settings'));
btnGroup.appendChild(btnRefresh);
btnGroup.appendChild(btnDiagnostic);
btnGroup.appendChild(btnSettings);
headerRight.appendChild(btnGroup);
header.appendChild(headerRight);
container.appendChild(header);
// Node.js version warning banner (shown if affected)
const warning = App.state.nodeVersionWarning;
if (warning && warning.affected) {
const banner = document.createElement('div');
banner.className = 'node-version-banner';
const iconDiv = document.createElement('div');
iconDiv.className = 'node-version-banner__icon';
iconDiv.appendChild(Helpers.icon('warning.svg', 24));
banner.appendChild(iconDiv);
const contentDiv = document.createElement('div');
contentDiv.className = 'node-version-banner__content';
const title = document.createElement('strong');
title.textContent = `Node.js ${warning.nodeVersion} — Streaming Incompatible`;
contentDiv.appendChild(title);
const textDiv = document.createElement('div');
textDiv.className = 'node-version-banner__text';
Helpers.appendNodeVersionWarning(textDiv);
contentDiv.appendChild(textDiv);
banner.appendChild(contentDiv);
container.appendChild(banner);
}
// Device grid
const stations = App.state.stations || [];
if (stations.length === 0) {
container.insertAdjacentHTML('beforeend', `
<div class="text-center text-muted py-5">
<div style="font-size: 2rem; margin-bottom: 12px;">${Helpers.iconHtml('inventory.svg', 32)}</div>
<p><strong>Your account is connected but no devices were found.</strong></p>
<p style="font-size: 0.85rem; max-width: 480px; margin: 0 auto;">
If you are using a guest admin account, make sure you have:<br>
1. Logged into the <strong>official Eufy Security app</strong> with the guest account<br>
2. <strong>Accepted the invitation</strong> from the main account<br>
3. <strong>Logged out</strong> of the official app before refreshing here
</p>
<button class="btn btn-primary btn-sm mt-3" id="btn-go-login">Refresh Devices</button>
</div>
`);
container.querySelector('#btn-go-login').addEventListener('click', () => App.navigate('login'));
return;
}
const grid = document.createElement('div');
grid.className = 'row';
// Flatten: stations + their devices
const ignoreDevices = config.ignoreDevices || [];
const ignoreStations = config.ignoreStations || [];
// Separate supported and unsupported
const supported = [];
const unsupported = [];
stations.forEach((station) => {
// Check if station has a standalone device (device serial === station serial)
const standaloneDevice = (station.devices || []).find(
(d) => d.uniqueId === station.uniqueId
);
if (standaloneDevice) {
// Standalone: show as single card (the device IS the station)
const item = { ...standaloneDevice, type: standaloneDevice.type };
if (station.unsupported || item.unsupported) {
item.unsupported = true;
unsupported.push({ item, isStation: false, station });
} else {
supported.push({ item, isStation: false, station });
}
} else {
// Station card
if (!station.disabled) {
const stationItem = {
uniqueId: station.uniqueId,
displayName: station.displayName,
type: station.type,
typename: station.typename,
unsupported: station.unsupported,
ignored: station.ignored,
power: station.power,
};
if (station.unsupported) {
unsupported.push({ item: stationItem, isStation: true, station });
} else {
supported.push({ item: stationItem, isStation: true, station });
}
}
// Device cards for this station
(station.devices || []).forEach((device) => {
if (station.unsupported || device.unsupported) {
unsupported.push({ item: { ...device, unsupported: true }, isStation: false, station });
} else {
supported.push({ item: device, isStation: false, station });
}
});
}
});
// Render supported devices
supported.forEach(({ item, isStation }) => {
const isIgnored = isStation
? ignoreStations.includes(item.uniqueId)
: ignoreDevices.includes(item.uniqueId);
DeviceCard.render(grid, {
device: { ...item, ignored: isIgnored },
isStation: isStation,
enabled: !isIgnored,
onClick: (d) => {
const type = isStation ? 'station' : 'device';
App.navigate('detail/' + type + '/' + d.uniqueId);
},
onToggle: (d, enabled) => {
const type = isStation ? 'station' : 'device';
Config.toggleIgnore(d.uniqueId, !enabled, type);
// Re-render to update visual state
this.render(this._container);
},
});
});
container.appendChild(grid);
// Unsupported section
if (unsupported.length > 0) {
const section = document.createElement('div');
section.className = 'mt-4';
section.innerHTML = `
<div class="detail-section__title">Unsupported Devices</div>
<p class="text-muted" style="font-size: 0.8rem;">
These devices were detected but are not yet fully supported by the eufy-security-client library.
Click on a device to find out how you can help us add support for it.
</p>
`;
const unsupportedGrid = document.createElement('div');
unsupportedGrid.className = 'row';
unsupported.forEach(({ item, isStation }) => {
DeviceCard.render(unsupportedGrid, {
device: { ...item, unsupported: true },
isStation: isStation,
enabled: false,
onClick: (d) => {
App.navigate('unsupported/' + d.uniqueId);
},
onToggle: () => {},
});
});
section.appendChild(unsupportedGrid);
container.appendChild(section);
}
},
_headerBtn(iconFile, label, btnClass, onClick) {
const btn = document.createElement('button');
btn.className = 'btn ' + btnClass + ' btn-sm eufy-tooltip';
btn.setAttribute('data-tooltip', label);
const img = document.createElement('img');
img.src = 'assets/icons/' + iconFile;
img.alt = label;
img.style.width = '24px';
img.style.height = '24px';
img.style.filter = 'brightness(0) invert(1)';
btn.appendChild(img);
btn.addEventListener('click', onClick);
return btn;
},
};