I’m currently working on a HomeWizard Cleaner app which (based on decompiling the mobile app) seems to work exactly the same as the HomeWizard Climate app, but then using the fan.homewizard.com domain instead of the cleaner.homewizard.com domain. This is the pairing logic for it (maybe replace type = cleaner with “fan“ or “climate“?)
session.setHandler("login", async (data) => {
try {
const email = data.email;
const password = data.password;
if (!data.email || !data.password) {
return false;
}
const basicAuth = "Basic " + Buffer.from(`${email}:${password}`).toString("base64");
const response = await axios.get('https://api.homewizardeasyonline.com/v1/auth/devices', {
headers: {
'Authorization': `${basicAuth}`
}
});
this.homey.settings.set('email', email);
this.homey.settings.set('password', password);
this.homey.settings.set('loggedIn', true);
await session.showView('list_devices');
return true;
} catch (error) {
if (error.response && error.response.status === 401) {
return false;
}
throw new Error("Error during API key check: " + error.message);
}
});
session.setHandler("list_devices", async (data) => {
try {
const email = this.homey.settings.get('email');
const password = this.homey.settings.get('password');
if (!email || !password) {
throw new Error("Email or password not found in storage.");
}
const basicAuth = "Basic " + Buffer.from(`${email}:${password}`).toString("base64");
const response = await axios.get('https://api.homewizardeasyonline.com/v1/auth/devices', {
headers: {
'Authorization': `${basicAuth}`
}
});
const links = response.data.devices.filter(d => d.type === "cleaner");
if (links.length === 0) {
return [];
}
return links.map(link => ({
name: link.name,
data: {
id: link.identifier,
},
store: {
id: link.identifier,
endpoint: link.endpoint
},
}));
} catch (error) {
throw new Error("Error while fetching links: " + error.message);
}
});
I don’t own the HomeWizard Cleaner device myself (it’s based on reverse engineering/modifying requests to “fake“ a HomeWizard Cleaner device with HTTP Toolkit).
The name “link“ was used as most of this is copied from my HomeWizard Link app. Device file (basic implementation of fetching status):
const email = this.homey.settings.get('email');
const password = this.homey.settings.get('password');
const basicAuth = "Basic " + Buffer.from(`${email}:${password}`).toString("base64");
const response = await axios.get('https://api.homewizardeasyonline.com/v1/auth/devices', {
headers: {
'Authorization': `${basicAuth}`
}
});
this.log('Fetched account info from API:', response.data);
const tokenResponse = await axios.post('https://api.homewizardeasyonline.com/v1/auth/token', {
device: this.getData().id
}, {
headers: {
'Authorization': `${basicAuth}`
}
});
this.log('Token response:', tokenResponse.data);
const token = tokenResponse.data.token;
const endpoint = this.getStoreValue('endpoint');
const statusResponse = await axios.get(`${endpoint}/`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
The device also exposes a Local API, but I never got discovery to work so that likely isn’t allowed on the App Store without discovery (to use local API, replace the endpoint from the deviceStoreValue with the local IP address).
The cloud endpoint is given by the API, but it’s likely https://fan.homewizard.com/<identifier (likely FAN_<MACADDRESS>)
For the Link, it would be https://<MACADDRESS>.homewizard.link/ and for the Cleaner it’s https://cleaner.homewizard.com/<CLNR_<MACADDRESS>
Edit: forgot to post the pairing view:
<!DOCTYPE html>
<html>
<head>
<style>
.error {
color: red;
display: none;
}
</style>
</head>
<body>
<fieldset class="homey-form-fieldset">
<legend class="homey-form-legend">HomeWizard Cleaner</legend>
<br>
<div class="homey-form-group">
<label class="homey-form-label" for="email" data-i18n="pair.email">Email address</label>
<input class="homey-form-input" id="email" type="text" value="" />
</div>
<div class="homey-form-group">
<label class="homey-form-label" for="password" data-i18n="pair.password">Password</label>
<input class="homey-form-input" id="password" type="password" value="" />
</div>
</fieldset>
<p id="failed" class="error" data-i18n="pair.failed">Invalid credentials</p>
<button id="save" class="homey-button-primary-full" data-i18n="settings.save">Save</button>
<script type="text/javascript">
const emailElement = document.getElementById('email');
const passwordElement = document.getElementById('password');
const errorElement = document.getElementById('failed');
const saveElement = document.getElementById('save');
errorElement.style.display = 'none';
saveElement.addEventListener("click", function (e) {
setSaving();
const email = emailElement.value;
const password = passwordElement.value;
Homey.emit("login", { email: email, password: password }).then(function (result) {
if (result === false) {
errorElement.style.display = 'block';
setSave();
}
});
});
function setSave() {
saveElement.className = 'homey-button-primary-full';
saveElement.textContent = Homey.__('pair.save');
}
function setSaving() {
saveElement.className = 'homey-button-primary-full is-loading';
saveElement.textContent = Homey.__('pair.saving');
}
</script>
</body>
</html>