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 outmaster
commit
29c4ed5f3c
@ -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…
Reference in New Issue