Toward the end of my time at Internet Crossroads, I was approached by a friend from my hometown with an idea for a project. He explained the opportunity and I began to create some mockups for them. They liked what I had come up with and working with them became my full time gig in short order. Before long, the business offered to move me out to my hometown and we were fast on our way to version two.
This new venture into HVAC was an interesting marriage of my web development skill set and an entirely different category. There are many forms of automated controls and many brands. Technically, the most ubiquitous standard for this industry is BACnet, an open-source communication protocol that is ratified by ASHRAE, but I found out quickly that different companies adhered to that standard in different ways. There is a core set of functionality that each controller is required to have in order to call itself BACnet compliant, but there's also LonTalk, Modbus, Allen-Bradley has their own network type, Zigbee has their own wireless standard, and many others.
This project entailed months of planning and development with many more months of fine tuning with customer feedback. Even with many clients using the product, the company I was contracted from ended up falling apart in early 2017. I was picked up as a full time employee of the company that had ordered the software originally.
This video shows internal communication from myself and a work colleague explaining how an end user might create an "HTTP Sender" with our current third party backend and have that match up with a remote Optimize server, and have the information land in a Tile and Component model that ends up showing data in our interface.
Even within BACnet, there is a two-wire network (MS/TP), BACnet over ethernet, and BACnet over IP. As you could probably guess, there are many opportunities for inconsistency of setup and bottlenecks among slower communication methods. Of these types, we eventually landed on BACnet over IP as our internal defacto standard, as it ended up playing nicer with different IT groups at different locations. They weren't particularly keen on allowing layer 2 traffic over their networks, and in some cases they were unable to route that traffic over certain VLANs, so this seemed to be the best compromise of speed and compatibility.
I can't stress enough how much of a rapid prototyping environment this was developed in. We were growing quickly and I had to make certain design concessions in order to get a product out.
At this point the company had acquired several additional clients, and they asked me to put together a promotional video of the interface. The video shows a later stage of development that includes Trend Log graph views of historic data, the export of that data, and the control of setpoints remotely.
<?php
namespace App\Http\Controllers;
use Gate;
use Carbon\Carbon;
use App\Http\Requests;
use App\Component;
use App\Device;
use Storage;
use Excel;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Cookie\CookieJar;
use App\Http\Controllers\Controller;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Http\Request as HttpRequest;
class TrendLogController extends Controller
{
public function requestTrendLog() {
if (Gate::denies('viewer')) {
abort( 403, 'You cannot view trend logs.' );
}
$components = Component::where('xid', '>', 0)->get();
$components = $components->each(function ($item) {
$deviceName = Device::find($item->device_id)->display_name;
$item['name'] = $deviceName . ' - ' . $item->details_name;
});
$components = $components->sortBy('name')->lists('name', 'id')->toArray();
return view('trend_logs.request', compact('components'));
}
/**
* @param HttpRequest $request
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function viewTrendLog(HttpRequest $request) {
if (Gate::denies('viewer')) {
abort( 403, 'You cannot view trend logs.' );
}
$startDateTime = Carbon::parse($request['start'])->tz('America/Los_Angeles')->format('Y-m-d\TH:i:s.uP'); // 2016-02-10T00:00:00.000-10:00
$endDateTime = Carbon::parse($request['end'])->tz('America/Los_Angeles')->format('Y-m-d\TH:i:s.uP');
$component = Component::where('id', $request['component'])->first();
$xid = $component->xid;
$deviceComponentName = Device::where('id', $component->device_id)->first()->display_name . ' - ' . $component->details_name;
$chartData = $this->getTrendLogJson($xid, $startDateTime, $endDateTime);
$chartData = collect(json_decode($chartData, true));
$request->flash();
return view('trend_logs.view', compact('chartData', 'deviceComponentName'));
}
public function exportTrendLog(HttpRequest $request) {
if (Gate::denies('viewer')) {
abort( 403, 'You cannot view trend logs.' );
}
// $chartData = Storage::get('test.json');
$startDateTime = Carbon::parse( $request->old( 'start' ) )->format('Y-m-d\TH:i:s.uP');
$endDateTime = Carbon::parse( $request->old( 'end' ) )->format('Y-m-d\TH:i:s.uP');
$component = Component::where('id', $request->old('component'))->first();
$xid = $component->xid;
// $deviceComponentName = Device::where('id', $component->device_id)->first()->display_name . ' - ' . $component->details_name;
$chartData = $this->getTrendLogJson( $xid, $startDateTime, $endDateTime );
// $chartData = Storage::get('test.json');
$chartData = json_decode( $chartData, true );
$plainStart = Carbon::parse( $request->old( 'start' ) );
$plainEnd = Carbon::parse( $request->old( 'end' ) );
Excel::create( "$plainStart to $plainEnd TL", function ( $excel ) use ( $chartData ) {
$excel->sheet( 'trend log', function ( $sheet ) use ( $chartData ) {
$sheet->loadView( 'trend_logs.excel' )->with( 'chartData', $chartData );
// $sheet->with(json_decode($chartData, true));
} );
} )->download( 'xls' );
}
/**
* Get Trend Log JSON
*
* @param $xid
*
* @param $startDateTime
* @param $endDateTime
*
* @return array|\Psr\Http\Message\StreamInterface
* @internal param $value
*/
public function getTrendLogJson($xid, $startDateTime, $endDateTime) {
if (Gate::denies('viewer')) {
abort(403, 'You cannot view trend logs.');
}
if ($xid == '') {
abort(403, 'You must declare an XID.');
}
$this->login();
$jar = session()->get( 'cookieJar' );
if($jar) {
$client = new Client( [ 'base_uri' => 'http://' . env('MANGO_IP') . '/rest/v1/', 'cookies' => $jar ] );
$headers = [
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip, deflate, sdch',
'Accept-Language' => 'en-US,en;q=0.8',
'Cache-Control' => 'no-cache',
'Content-Type' => 'application/json',
'Connection' => 'keep-alive',
];
try {
$response = $client->request( 'GET', "point-values/" .
$xid .
'?useRendered=false' .
'&unitConversion=false' .
'&from=' .
$startDateTime . // 2016-02-10T00:00:00.000-10:00
'&to=' .
$endDateTime . // 2016-02-11T23:59:59.999-10:00
'&rollup=NONE&timePeriodType=MINUTES&timePeriods=15'
, [
'headers' => $headers,
// 'body' => $json,
] );
return $response->getBody();
} catch( RequestException $e ) {
$jsonResponse[] = $e->getRequest()->getHeaders();
if ( $e->hasResponse() ) {
$jsonResponse[] = $e->getResponse()->getHeaders();
}
return json_encode( $jsonResponse );
} catch( ClientException $e ) {
$jsonResponse[] = $e->getRequest()->getHeaders();
if ( $e->hasResponse() ) {
$jsonResponse[] = $e->getResponse()->getHeaders();
}
return json_encode( $jsonResponse );
}
} else {
return 'Try logging in again.';
}
}
public function login() {
$headers = [
'Accept' => 'application/json',
'Password' => env('MANGO_PW'),
];
$jar = new CookieJar;
$client = new Client( [ 'base_uri' => 'http://' . env('MANGO_IP') . '/rest/v1/', 'cookies' => $jar ] );
session()->put( 'cookieJar', $jar );
$client->get( 'login/' . env('MANGO_USER'), [ 'headers' => $headers ] );
}
}
<?php
namespace App\Http\Controllers;
use Gate;
use Request;
use File;
use App\Device;
use App\Component;
use App\Http\Requests;
use App\DeviceTemplate;
use App\StatCollection;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request as HttpRequest;
use App\Http\Requests\ComponentRequest;
class ComponentsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
if (Gate::denies('admin')) {
abort(403, 'You cannot edit components.');
}
return view('components.index');
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function readings()
{
if (Gate::denies('viewer')) {
abort(403, 'You cannot view components.');
}
return view('tabs');
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
if (Gate::denies('admin')) {
abort(403, 'You cannot edit components.');
}
return view('components.create');
}
/**
* Store a newly created resource in storage.
*
* @param ComponentRequest $request
*
* @return \Illuminate\Http\Response
*/
public function store(ComponentRequest $request)
{
if (Gate::denies('admin')) {
abort(403, 'You cannot edit components.');
}
$vue_name = str_replace(
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', ' ', '_', '/'],
['ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', '', '', '', ''],
$request->name);
$request['vue_name'] = $vue_name;
$component = Component::create($request->all());
if($request['tenant_list'] === NULL || $request['tenant_list'] === '' ) {
$request['tenant_list'] = [];
}
$component->tenants()->sync($request['tenant_list']);
return redirect('components');
}
/**
* Show the form for editing the specified resource.
*
* @param Component $component
*
* @return \Illuminate\Http\Response
*
*/
public function edit(Component $component)
{
if (Gate::denies('admin')) {
abort(403, 'You cannot edit component templates.');
}
return view('components.edit', compact('component'));
}
/**
* Update the specified resource in storage.
*
* @param ComponentRequest $request
* @param Component $component
*
* @return \Illuminate\Http\Response
*/
public function update(ComponentRequest $request, Component $component)
{
if (Gate::denies('admin')) {
abort(403, 'You cannot edit components.');
}
$vue_name = str_replace(
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', ' ', '_', '/'],
['ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', '', '', '', ''],
$request->name);
$request['vue_name'] = $vue_name;
$component->update($request->all());
if($request['tenant_list'] === NULL || $request['tenant_list'] === '' ) {
$request['tenant_list'] = [];
}
$component->tenants()->sync($request['tenant_list']);
return redirect('components');
}
/**
* Remove the specified resource from storage.
*
* @param Component $component
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \Exception
*/
public function destroy(Component $component)
{
if (Gate::denies('admin')) {
abort(403, 'You cannot delete components.');
}
$component->tenants()->detach($component->tenants->lists('id')->all());
$component->delete();
return redirect('/components');
}
}