Add first (and only?) file

shell of the app. awaiting api access

Tidy up colours and radio buttons

Autofocus on username. Change default hop to 6am

Add self verification process

Add license. More tidying up.

All that self-verification stuff was nonsense

removed

Get working with API

Adjust styles of buttons. Change done message.

Highlight selected hop

API integration for changing hour of power

Rounded buttons

Change logout to log out
master
Damian Peterson 2 years ago
commit 29c4ed5f3c

1
.gitignore vendored

@ -0,0 +1 @@
.env

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,457 @@
<?php
/*
Electric Kiwi HourChanger
Not affiliated with Electric Kiwi the company.
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'];
$customerNumber = '';
$connectionId = '';
$customerName = '';
/**
* Get session details from the API. Sets customer number, connection ID and customer name.
*
* @param $vars
* @param $accessToken
* @param $customerNumber
* @param $connectionId
* @param $customerName
* @return void
* @throws Exception
*/
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.
*
* @param $vars
* @param $accessToken
* @param $customerNumber
* @param $connectionId
* @param $selectedHour
* @return void
* @throws Exception
*/
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.
*
* @param $vars
* @param $accessToken
* @param $customerNumber
* @param $connectionId
* @param $selectedHour
* @return void
* @throws Exception
*/
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.
*
* @param $vars
* @param $code
* @return void
* @throws Exception
*/
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.
*
* @param $vars
* @param $refreshToken
* @return void
* @throws Exception
*/
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: /');
}
}
// 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);
$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);
$message = 'Done. Hour changed to ' . $times[$selectedHour] . '.';
} 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);
}
.hours {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 20px;
}
.hour {
white-space: nowrap;
}
.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;
}
.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);
}
.info {
color: var(--message-color);
font-size: 0.9rem;
font-style: italic;
}
</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'] ?>&redirect_uri=<?= $vars['REDIRECT_URL'] ?>&scope=<?= urlencode($vars['SCOPES']) ?>"
class="button">Log in now</a>
</div>
<?php else: ?>
<form action="" method="post" class="update_form">
<?php if ($message): ?>
<div class="message"><?= $message ?></div>
<?php endif; ?>
<div class="hours">
<?php foreach ($times as $key => $value): ?>
<div 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>
</div>
<?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 in order to allow you to
update your hour of power. None of your personal information is retained or shared.
<a href="https://git.peterson.nz/damian/HourChanger">Source code</a>
</p>
</div>
</body>
</html>
Loading…
Cancel
Save