/*****************************
* Heat Control program      *
* Last updated: 18 Oct 2014 *
* Author: Mateusz Viste     *
*****************************

Change log:
  [18 Oct 2014]
    - Fixed segfaults occuring when data files do not exist
    - Slot changes are applied within a few seconds (was: 5 minutes later)
  [24 Nov 2013]
    - Rewritten in ANSI C.
  [11 Nov 2009]
    - Added support for a 4th zone.
  [18 Oct 2009]
    - First public release.
*/

#include <errno.h>
#include <stdio.h>    /* printf()... */
#include <time.h>     /* time() */
#include <string.h>   /* strerror() */
#include <sys/io.h>   /* outb() */
#include <stdlib.h>   /* calloc() */
#include <unistd.h>   /* sleep() */

enum zonestate {
 MODE_OFF = 0,
 MODE_NOFREEZE = 1,
 MODE_ECO = 2,
 MODE_COMFORT = 3,
 MODE_INIT = 255
};

/* This struct will hold all our 'shared' variables */
struct heatdata {
 enum zonestate WeekProgram[4][7][96];
 enum zonestate ForcedZone[4];
 enum zonestate CurZoneState[4];
 time_t ForcedTime[4];
 time_t LastConfReload;
 time_t LastForceCheck;
 int portaddr;
 int activeslot;
};


/* Declare all routines and functions */

void LoadWeekProgram(struct heatdata *data) {
 char tmpstring[128];
 int x, y, z;
 FILE *fd;
 /* load weekly data */
 snprintf(tmpstring, 128, "/etc/heatcontrol/%d.cfg", data->activeslot);
 fd = fopen(tmpstring, "r");
 if (fd == NULL) return;
 for (x = 0; x < 4; x++) {       /* Zones 1-4 */
   for (y = 0; y < 7; y++) {     /* Weekdays 1-7 */
     for (z = 0; z < 96; z++) {  /* Time spaces 1-96 (24h) */
       fscanf(fd, "%u", &(data->WeekProgram[x][y][z]));
     }
   }
 }
 fclose(fd);
}


int getActiveSlot(void) {
 FILE *fd;
 int slot = 0;
 /* fetch the active slot number */
 fd = fopen("/etc/heatcontrol/active.cfg", "r");
 if (fd != NULL) {
   fscanf(fd, "%d", &slot);
   fclose(fd);
 }
 return(slot);
}


/* Returns the week of the day (0-6, where 0 = Monday) */
int CurrentWeekDay(time_t t) {
 int result;
 result = localtime(&t)->tm_wday;
 if (result == 0) result = 7;
 result -= 1;
 return(result);
}


/* returns the current 15-minute slice of the day (0..95) */
int CurrentTimeSlice(time_t curtime) {
 int result;
 struct tm *brokentime;
 brokentime = localtime(&curtime);
 result = brokentime->tm_hour * 60;
 result += brokentime->tm_min;
 result /= 15;    /* compute the Nth 15-minutes slice of the day */
 return(result);
}


void CheckForConfUpdate(struct heatdata *data, time_t curtime) {
 int x;
 FILE *fd;
 /* Check for forced modes every 15 seconds */
 if ((curtime - data->LastForceCheck) > 15) {
   data->LastForceCheck = curtime;
   fd = fopen("/etc/heatcontrol/forcing", "r");
   if (fd == NULL) return;
   for (x = 0; x < 4; x++) {
     fscanf(fd, "%u", &(data->ForcedZone[x]));
   }
   for (x = 0; x < 4; x++) {
     fscanf(fd, "%ld", &(data->ForcedTime[x]));
   }
   fclose(fd);
 }
 /* reload the configuration every 5 minutes, or as soon a new slot is
  * activated. */
 x = getActiveSlot();
 if ((x != data->activeslot) || ((curtime - data->LastConfReload) > 300)) {
   data->activeslot = x;
   data->LastConfReload = curtime;
   LoadWeekProgram(data);
 }
}


void UpdateCurState(struct heatdata *data) {
 FILE *fd;
 int x;
 fd = fopen("/etc/heatcontrol/curstate.dat", "w");
 if (fd == NULL) return;
 for (x = 0; x < 4; x++) {
   fprintf(fd, "%d\n", data->CurZoneState[x]);
 }
 fclose(fd);
}


void UpdateParPort(struct heatdata *data) {
 /* Update the parallel port status */
 int ByteDriver = 0;
 int x;
 for (x = 0; x < 4; x++) {
   /* Prepare the bit fields for all zones */
   switch (data->CurZoneState[x]) {
     case MODE_OFF: /* Off */
       ByteDriver |= (2 << (x * 2));
       break;
     case MODE_NOFREEZE: /* Hors Gel */
       ByteDriver |= (1 << (x * 2));
       break;
     case MODE_ECO: /* Eco */
       ByteDriver |= (3 << (x * 2));
       break;
     case MODE_COMFORT: /* Comfort */
       /* No bit to set. Leave at zero. */
       break;
     case MODE_INIT:    /* NOOP */
       break;
   }
 }
 /* Send the byte to the parallel port now */
 outb(ByteDriver, data->portaddr);
}


void ParPortTestProcedure(int portaddr) {
 outb(0, portaddr);    /* sends the byte 0 to LPT port (clear all pins) */
 sleep(1);
 outb(255, portaddr);  /* All pins to ON */
 sleep(1);
 outb(0, portaddr);    /* sends the byte 0 to LPT port (clear all pins) */
 sleep(5);             /* wait 5 seconds before starting the test */
 outb(1, portaddr);
 sleep(2);
 outb(2, portaddr);
 sleep(2);
 outb(4, portaddr);
 sleep(2);
 outb(8, portaddr);
 sleep(2);
 outb(16, portaddr);
 sleep(2);
 outb(32, portaddr);
 sleep(2);
 outb(64, portaddr);
 sleep(2);
 outb(128, portaddr);
 sleep(2);
 outb(0, portaddr);    /* sends the byte 0 to LPT port (clear all pins) */
}


/* Here starts the real program */

int main(int argc, char **argv) {
 struct heatdata *data;
 enum zonestate OldZoneState[4];
 time_t curtime;
 int curwday, curtslice, x;

 if (argc != 2) {
   printf("Invalid number of arguments!\n");
   printf("Example: heatcontrol 0x3BC\n");
   return(4);
 }

 /* allocate memory for variables */
 data = calloc(1, sizeof(struct heatdata));
 if (data == NULL) {
   printf("Failed to allocate memory. Out of memory?\n");
   return(3);
 }

 /* Load current zone states to init values */
 for (x = 0; x < 4; x++) data->CurZoneState[x] = MODE_INIT;

 /* Read the port address, and convert it into decimal. */
 data->portaddr = strtol(argv[1], NULL, 16);

 if (data->portaddr < 1) {
   printf("Invalid port argument. Example: heatcontrol 0x3BC\n");
   printf("Program aborted.\n");
   return(1);
 }

 /* Load the heating configuration */
 data->activeslot = getActiveSlot();
 LoadWeekProgram(data);

 printf("HeatControl started on hardware port 0x%03X\n", data->portaddr);

 if (ioperm(data->portaddr, 1, 1) != 0) {  /* turn on access to port xxxx */
   printf("ERROR: ioperm() failed on 0x%03X (%s)\n", data->portaddr, strerror(errno));
   return(2);
 }
 ParPortTestProcedure(data->portaddr);

 for (;;) {
   sleep(5);   /* Wait five seconds before re-looping */
   curtime = time(NULL);
   curwday = CurrentWeekDay(curtime);
   curtslice = CurrentTimeSlice(curtime);
   CheckForConfUpdate(data, curtime);
   /* printf("CurrentWeekDay(): %d / CurrentTimeSlice: %d\n", curwday, curtslice); */
   for (x = 0; x < 4; x++) {
     /* Save current zones states to a temporary buffer and refresh current value */
     OldZoneState[x] = data->CurZoneState[x];
     data->CurZoneState[x] = data->WeekProgram[x][curwday][curtslice];
     /* check if the zone is forced currently */
     if (data->ForcedTime[x] - curtime > 0) data->CurZoneState[x] = data->ForcedZone[x];
     /* printf("Zone %d = %d\n", x, data->CurZoneState[x]); */
   }
   /* Now check if anything changed - if so, send an update to the LPT port */
   if ((data->CurZoneState[0] != OldZoneState[0]) || (data->CurZoneState[1] != OldZoneState[1]) || (data->CurZoneState[2] != OldZoneState[2]) || (data->CurZoneState[3] != OldZoneState[3])) {
     UpdateCurState(data);
     UpdateParPort(data);
   }
 }
}