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

<?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>