You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
429 lines
15 KiB
PHP
429 lines
15 KiB
PHP
<?php
|
|
/*
|
|
Electric Kiwi HourChanger
|
|
|
|
Not affiliated with Electric Kiwi.
|
|
|
|
Uses the Electric Kiwi API to update a customer's hour of free power. Created because the web page provided by the
|
|
company became a 12MB JS-only web app that broke integration with common password managers.
|
|
|
|
----
|
|
|
|
The MIT License (MIT)
|
|
Copyright (c) 2023 Damian Peterson
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
|
Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
// load variables for connecting to the Electric Kiwi API
|
|
$vars = parse_ini_file('.env');
|
|
|
|
if (!$vars) {
|
|
echo "Missing .env file containing core variables";
|
|
die();
|
|
}
|
|
|
|
// Build a list of selectable time slots
|
|
$times = [];
|
|
|
|
// Valid time slots are between 9am-5pm and 9pm-7am. 11:30pm is also excluded
|
|
$excludedTimes = [14, 15, 16, 17, 18, 34, 35, 36, 37, 38, 39, 40, 41, 42, 48];
|
|
|
|
for ($i = 1; $i <= 48; $i++) {
|
|
if (!in_array($i, $excludedTimes)) {
|
|
$times[$i] = sprintf('%s:%s', str_pad(floor(($i - 1) / 2), 2, 0, STR_PAD_LEFT), $i % 2 ? '00' : '30');
|
|
}
|
|
}
|
|
|
|
// Other key variables
|
|
$message = 'To set your hour of power you will need to log in to your Electric Kiwi account';
|
|
$selectedHour = 13;
|
|
$isLoggedIn = true;
|
|
$accessToken = $_COOKIE['access_token'];
|
|
$refreshToken = $_COOKIE['refresh_token'];
|
|
$newHour = $_COOKIE['hour'];
|
|
$customerNumber = '';
|
|
$connectionId = '';
|
|
$customerName = '';
|
|
|
|
/**
|
|
* Get session details from the API. Sets customer number, connection ID and customer name.
|
|
*/
|
|
function getCustomerDetails($vars, $accessToken, &$customerNumber, &$connectionId, &$customerName) {
|
|
$curl = curl_init();
|
|
curl_setopt($curl, CURLOPT_URL, $vars['API_URL'] . 'session/');
|
|
curl_setopt($curl, CURLOPT_POST, 0);
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $accessToken, 'Content-Type: application/json']);
|
|
$session = curl_exec($curl);
|
|
curl_close($curl);
|
|
|
|
if (!$session) {
|
|
throw new Exception('Failed to get customer details');
|
|
}
|
|
|
|
$jsonSession = json_decode($session, true);
|
|
if ($jsonSession['error']) {
|
|
throw new Exception($jsonSession['error']['detail'], $jsonSession['error']['code']);
|
|
} else {
|
|
$customerNumber = $jsonSession['data']['customer'][0]['customer_number'];
|
|
$connectionId = $jsonSession['data']['customer'][0]['connection']['connection_id'];
|
|
$customerName = $jsonSession['data']['customer'][0]['first_name'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the customer's current hour of power settings.
|
|
*/
|
|
function getCurrentHour($vars, $accessToken, $customerNumber, $connectionId, &$selectedHour) {
|
|
$curl = curl_init();
|
|
curl_setopt($curl, CURLOPT_URL, $vars['API_URL'] . 'hop/' . $customerNumber . '/' . $connectionId .'/');
|
|
curl_setopt($curl, CURLOPT_POST, 0);
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $accessToken, 'Content-Type: application/json']);
|
|
$hourDetails = curl_exec($curl);
|
|
curl_close($curl);
|
|
|
|
if (!$hourDetails) {
|
|
throw new Exception('Failed to get selected hour details');
|
|
}
|
|
|
|
$jsonHourDetails = json_decode($hourDetails, true);
|
|
if ($jsonHourDetails['error']) {
|
|
throw new Exception($jsonHourDetails['error']['detail'], $jsonHourDetails['error']['code']);
|
|
} else {
|
|
$selectedHour = $jsonHourDetails['data']['start']['interval'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set new hour of power preference for a customer connection.
|
|
*/
|
|
function setCurrentHour($vars, $accessToken, $customerNumber, $connectionId, &$selectedHour) {
|
|
$post = [
|
|
'start' => $selectedHour
|
|
];
|
|
$curl = curl_init();
|
|
curl_setopt($curl, CURLOPT_URL, $vars['API_URL'] . 'hop/' . $customerNumber . '/' . $connectionId .'/');
|
|
curl_setopt($curl, CURLOPT_POST, 1);
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
|
|
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $accessToken, 'Accept: application/json']);
|
|
$hourDetails = curl_exec($curl);
|
|
curl_close($curl);
|
|
|
|
if (!$hourDetails) {
|
|
throw new Exception('Failed to get selected hour details');
|
|
}
|
|
|
|
$jsonHourDetails = json_decode($hourDetails, true);
|
|
if ($jsonHourDetails['error']) {
|
|
throw new Exception($jsonHourDetails['error']['detail'], $jsonHourDetails['error']['code']);
|
|
} else {
|
|
$selectedHour = $jsonHourDetails['data']['start']['interval'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get access and refresh tokens for a customer from their login code and cookie them.
|
|
*/
|
|
function authorizeWithCode($vars, $code) {
|
|
$post = [
|
|
'code' => $code,
|
|
'client_id' => $vars['CLIENT_ID'],
|
|
'client_secret' => $vars['CLIENT_SECRET'],
|
|
'grant_type' => 'authorization_code',
|
|
'scope' => $vars['SCOPES'],
|
|
'redirect_uri' => $vars['REDIRECT_URI'],
|
|
];
|
|
$curl = curl_init();
|
|
curl_setopt($curl, CURLOPT_URL, $vars['TOKEN_URL']);
|
|
curl_setopt($curl, CURLOPT_POST, 1);
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
|
|
$auth = curl_exec($curl);
|
|
curl_close($curl);
|
|
|
|
if (!$auth) {
|
|
throw new Exception('Post to authorize failed', 500);
|
|
}
|
|
|
|
$jsonAuth = json_decode($auth, true);
|
|
|
|
if ($jsonAuth['error']) {
|
|
throw new Exception($jsonAuth['error']['detail'], $jsonAuth['error']['code']);
|
|
} else {
|
|
setcookie('access_token', $jsonAuth['access_token'], time() + $jsonAuth['expires_in'], "/");
|
|
setcookie('refresh_token', $jsonAuth['refresh_token'], time() + (86400 * 90), "/");
|
|
header('Location: /');
|
|
die();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh a customer's access token from their refresh token.
|
|
*/
|
|
function refreshToken($vars, $refreshToken) {
|
|
$post = [
|
|
'grant_type' => 'refresh_token',
|
|
'refresh_token' => $refreshToken,
|
|
];
|
|
$curl = curl_init();
|
|
curl_setopt($curl, CURLOPT_URL, $vars['TOKEN_URL']);
|
|
curl_setopt($curl, CURLOPT_POST, 1);
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
|
|
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Basic ' . base64_encode($vars['CLIENT_ID'] . ':' . $vars['CLIENT_SECRET']), 'Content-Type: multipart/form-data']);
|
|
$auth = curl_exec($curl);
|
|
curl_close($curl);
|
|
|
|
if (!$auth) {
|
|
setcookie('refresh_token', '', time() - 1000, "/");
|
|
throw new Exception('Failed to log back in automatically', 500);
|
|
}
|
|
|
|
$jsonAuth = json_decode($auth, true);
|
|
|
|
if ($jsonAuth['error']) {
|
|
setcookie('refresh_token', '', time() - 1000, "/");
|
|
throw new Exception('Failed to log back in automatically (' . $auth . ')', 500);
|
|
} else {
|
|
setcookie('access_token', $jsonAuth['access_token'], time() + $jsonAuth['expires_in'], "/");
|
|
setcookie('refresh_token', $jsonAuth['refresh_token'], time() + (86400 * 90), "/");
|
|
header('Location: /');
|
|
die();
|
|
}
|
|
}
|
|
|
|
// We've been pinged by Electric Kiwi with a new code. We need to get a token, cookie it and redirect to the homepage.
|
|
if ($_GET && key_exists('code', $_GET)) {
|
|
try {
|
|
authorizeWithCode($vars, $_GET['code']);
|
|
} catch (Exception $exception) {
|
|
$message = 'There was a problem signing in. Perhaps try again.';
|
|
$isLoggedIn = false;
|
|
}
|
|
}
|
|
|
|
// Logout has been requested so destroy access and refresh token cookies
|
|
if ($_GET && key_exists('logout', $_GET)) {
|
|
setcookie('access_token', '', time() - 1000, "/");
|
|
setcookie('refresh_token', '', time() - 1000, "/");
|
|
header('Location: /');
|
|
}
|
|
|
|
// Set state of landing page depending on whether customer has tokens
|
|
if (!isset($accessToken) && !isset($refreshToken)) {
|
|
// This user doesn't have a saved access or refresh token. Prompt login
|
|
$isLoggedIn = false;
|
|
} elseif (isset($accessToken) && isset($refreshToken)) {
|
|
// They have an access token so attempt to get customer details
|
|
try {
|
|
getCustomerDetails($vars, $accessToken, $customerNumber, $connectionId, $customerName);
|
|
getCurrentHour($vars, $accessToken, $customerNumber, $connectionId, $selectedHour);
|
|
if ($newHour) {
|
|
$message = 'Done, changed to ' . $times[$newHour] . '.';
|
|
setcookie('hour', '', time() - 1000, "/");
|
|
} else {
|
|
$message = 'Select your hour of power.';
|
|
}
|
|
} catch (Exception $exception) {
|
|
if ($exception->getCode() === 401) {
|
|
try {
|
|
refreshToken($vars, $refreshToken);
|
|
} catch (Exception $exception) {
|
|
$message = $exception->getMessage() . '. Please log in again below.';
|
|
$isLoggedIn = false;
|
|
}
|
|
}
|
|
}
|
|
} elseif (isset($refreshToken)) {
|
|
try {
|
|
refreshToken($vars, $refreshToken);
|
|
} catch (Exception $exception) {
|
|
$message = $exception->getMessage() . '. Please log in again below.';
|
|
$isLoggedIn = false;
|
|
}
|
|
}
|
|
|
|
// The form was submitted. Update the hour of power
|
|
if ($isLoggedIn && $_POST) {
|
|
// Wanting to update hour of power
|
|
$selectedHour = $_POST['hour'];
|
|
$isValid = true;
|
|
|
|
if (empty($selectedHour)) {
|
|
$message = 'Please select an hour for your hour of free power';
|
|
$isValid = false;
|
|
}
|
|
|
|
if ($isValid) {
|
|
try {
|
|
setCurrentHour($vars, $accessToken, $customerNumber, $connectionId, $selectedHour);
|
|
setcookie('hour', $selectedHour, time() + 30000, "/");
|
|
header('Location: /');
|
|
die();
|
|
} catch (Exception $exception) {
|
|
$message = $exception->getMessage();
|
|
}
|
|
}
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en_NZ">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Electric Kiwi Hour Changer</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
:root {
|
|
--color-light: #ffffff;
|
|
--color-light-grey: #cccccc;
|
|
--color-dark-grey: #555555;
|
|
--color-dark: #111111;
|
|
--color-primary: firebrick;
|
|
--text-color: var(--color-dark);
|
|
--bg-color: var(--color-light);
|
|
--message-color: var(--color-dark-grey);
|
|
--selected-color: var(--color-light-grey);
|
|
--btn-color: var(--color-light);
|
|
--btn-bg-color: var(--color-primary);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--text-color: var(--color-light);
|
|
--bg-color: var(--color-dark);
|
|
--message-color: var(--color-light-grey);
|
|
--selected-color: var(--color-dark-grey);
|
|
}
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
font-size: 1.1rem;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.container {
|
|
max-width: 360px;
|
|
margin: 20px;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
a {
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.message {
|
|
color: var(--message-color);
|
|
font-style: italic;
|
|
padding-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
border-bottom: 1px solid var(--color-primary);
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.button {
|
|
padding: 20px;
|
|
background-color: var(--btn-bg-color);
|
|
border-radius: 100px;
|
|
color: var(--btn-color);
|
|
text-decoration: none;
|
|
border: none;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.info {
|
|
color: var(--message-color);
|
|
font-size: 0.9rem;
|
|
font-style: italic;
|
|
}
|
|
|
|
<?php if ($isLoggedIn): ?>
|
|
.hours {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.hour {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.hour label {
|
|
padding: 10px;
|
|
cursor: pointer;
|
|
display: block;
|
|
border-radius: 30px;
|
|
}
|
|
|
|
.hour input {
|
|
display: none;
|
|
}
|
|
|
|
.hour.selected label {
|
|
background-color: var(--selected-color)
|
|
}
|
|
|
|
.hours input:checked + label {
|
|
background-color: var(--btn-bg-color);
|
|
color: var(--btn-color);
|
|
}
|
|
<?php endif; ?>
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<?php if (!$isLoggedIn): ?>
|
|
<?php if ($message): ?><div class="message"><?= $message ?></div><?php endif; ?>
|
|
<div class="actions">
|
|
<a href="<?= $vars['AUTHORIZE_URL']; ?>?response_type=code&client_id=<?= $vars['CLIENT_ID'] ?>&scope=<?= urlencode($vars['SCOPES']) ?>" class="button">Log in now</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<form action="" method="post">
|
|
<?php if ($message): ?><div class="message"><?= $message ?></div><?php endif; ?>
|
|
<div class="hours">
|
|
<?php foreach ($times as $key => $value): ?>
|
|
<span class="hour<?php if ($key == $selectedHour): ?> selected<?php endif; ?>">
|
|
<input type="radio" id="hour<?= $key ?>" name="hour" value="<?= $key ?>"<?php if ($key == $selectedHour): ?> checked<?php endif; ?>>
|
|
<label for="hour<?= $key ?>" title="<?= $value ?>"><?= $value ?></label>
|
|
</span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="actions">
|
|
<button type="submit" class="button" name="update_action">Update your hour of power</button>
|
|
</div>
|
|
</form>
|
|
<p>Logged in as <?= $customerName ?>. <a href="?logout" class="logout">Log out</a></p>
|
|
<?php endif; ?>
|
|
<p class="info">This site is not affiliated with Electric Kiwi. It uses Electric Kiwi's API to
|
|
update your hour of power. None of your personal information is retained or shared.<br />
|
|
<a href="https://git.peterson.nz/damian/HourChanger">Source code</a>
|
|
</p>
|
|
</div>
|
|
</body>
|
|
</html>
|