residential/waterheater.cpp

Go to the documentation of this file.
00001 
00021 #include <stdlib.h>
00022 #include <stdio.h>
00023 #include <errno.h>
00024 #include <math.h>
00025 
00026 #include "house.h"
00027 #include "waterheater.h"
00028 
00029 #define TSTAT_PRECISION 0.01
00030 #define HEIGHT_PRECISION 0.01
00031 
00033 // underground_line_conductor CLASS FUNCTIONS
00035 CLASS* waterheater::oclass = NULL;
00036 waterheater *waterheater::defaults = NULL;
00037 
00040 waterheater::waterheater(MODULE *module) 
00041 {
00042     // first time init
00043     if (oclass==NULL)
00044     {
00045         // register the class definition
00046         oclass = gl_register_class(module,"waterheater",PC_BOTTOMUP);
00047         if (oclass==NULL)
00048             GL_THROW("unable to register object class implemented by %s",__FILE__);
00049 
00050         // publish the class properties
00051         if (gl_publish_variable(oclass,
00052             PT_double,"tank_volume[gal]",PADDR(tank_volume),
00053             PT_double,"tank_UA[Btu/h]",PADDR(tank_UA),
00054             PT_double,"tank_diameter[ft]",PADDR(tank_diameter),
00055             PT_double,"water_demand[gpm]",PADDR(water_demand),
00056             PT_double,"heating_element_capacity[W]",PADDR(heating_element_capacity),
00057             PT_double,"inlet_water_temperature[degF]",PADDR(Tinlet),
00058             PT_enumeration,"location",PADDR(location),
00059                 PT_KEYWORD,"INSIDE",INSIDE,
00060                 PT_KEYWORD,"GARAGE",GARAGE,
00061             PT_double,"tank_setpoint[degF]",PADDR(tank_setpoint),
00062             PT_double,"thermostat_deadband[degF]",PADDR(thermostat_deadband),
00063             PT_complex,"power[kW]",PADDR(power_kw),
00064             PT_double,"meter[kWh]",PADDR(kwh_meter),
00065             PT_complex,"meter[kWh]",PADDR(kwh_meter),
00066             PT_double,"temperature[degF]",PADDR(Tw),
00067             NULL)<1) 
00068             GL_THROW("unable to publish properties in %s",__FILE__);
00069 
00070         // setup the default values
00071         defaults = this;
00072 
00073         // initialize public values
00074         tank_volume = 50.0;
00075         tank_UA = 0.0;
00076         tank_diameter   = 1.5;  // All heaters are 1.5-ft wide for now...
00077         Tinlet = 60.0;      // default set here, but published by the model for users to set this value
00078         water_demand = 0.0; // in gpm
00079         heating_element_capacity = 0.0;
00080         heat_needed = FALSE;
00081         location = GARAGE;
00082         tank_setpoint = 0.0;
00083         thermostat_deadband = 0.0;
00084         power_kw = complex(0,0);
00085         kwh_meter = 0.0;
00086     }
00087 }
00088 
00089 waterheater::~waterheater()
00090 {
00091 }
00092 
00093 int waterheater::create() 
00094 {
00095     // copy the defaults
00096     memcpy(this,defaults,sizeof(*this));
00097 
00098     // location...mostly in garage, a few inside...
00099     location = gl_random_bernoulli(0.80) ? GARAGE : INSIDE;
00100 
00101     // initialize randomly distributed values
00102     tank_setpoint       = clip(gl_random_normal(130,10),100,160);
00103     thermostat_deadband = clip(gl_random_normal(5, 1),1,10);
00104     return 1;
00105 }
00106 
00109 int waterheater::init(OBJECT *parent)
00110 {
00111     if (parent==NULL)
00112     {
00113         gl_error("waterheater must have a parent house");
00114         return 0;
00115     }
00116     pHouse = OBJECTDATA(parent,house);
00117 
00118     // attach object to house panel
00119     pVoltage = (pHouse->attach(OBJECTHDR(this),30,TRUE))->pV; // 220V 30amp breaker
00120 
00121     // basic randomized defaults for published variables, if user has not set any of these
00122     // Assuming a 50-gal heater with a UA of 2.0 Btu/hr-F for a 2000-sf house.  
00123     if (tank_volume <= 0.0)
00124         tank_volume = 5*floor((1.0/5.0)*gl_random_uniform(0.90, 1.10) * 50.0 * (pHouse->get_floor_area() /2000.0));  // [gal]
00125 
00126     // truncate tank volume distribution
00127     if (tank_volume > 100.0)
00128         tank_volume = 100.0;        
00129     else if (tank_volume < 40.0) 
00130         tank_volume = 40.0;
00131 
00132     // initial tank UA
00133     if (tank_UA <= 0.0)
00134 
00135         // UA-2.0 represents R-13; assume UA varies directly with volume...
00136         tank_UA = clip(gl_random_normal(2.0, 0.20),0.1,10) * tank_volume/50;  
00137 
00138     // Set heating element capacity if not provided by the user
00139     if (heating_element_capacity <= 0.0)
00140     {
00141         if (tank_volume >= 50)
00142             heating_element_capacity = 4500;
00143         else 
00144         {
00145             // Smaller tanks can be either 3200, 3500, or 4500...
00146             double randVal = gl_random_uniform(0,1);
00147             if (randVal < 0.33)
00148                 heating_element_capacity = 3200;
00149             else if (randVal < 0.67)
00150                 heating_element_capacity = 3500;
00151             else
00152                 heating_element_capacity = 4500;
00153         }
00154     }
00155 
00156     // Other initial conditions
00157     Tw = 122.0; //gl_random_uniform(tank_setpoint - thermostat_deadband, tank_setpoint + thermostat_deadband);
00158     current_model = NONE;
00159     load_state = STABLE;
00160 
00161     // initial demand
00162     Tset_curtail    = tank_setpoint - thermostat_deadband/2 - 10;  // Allow T to drop only 10 degrees below lower cut-in T...
00163 
00164     // heating capacity
00165     heating_element_capacity *= BTUPHPW; // Convert to [Btu/W-hr]...
00166     tank_volume /= GALPCF;  // Convert to [ft^3] for all other work...
00167 
00168     // Setup derived characteristics...
00169     area        = (pi * pow(tank_diameter,2))/4;
00170     height      = tank_volume / area;
00171     Cw          = tank_volume * RHOWATER * Cp;  // [Btu/F]
00172 
00173     // initial demand
00174     cur_water_demand = last_water_demand = water_demand;
00175 
00176     // @todo initial tank charge should be based on demand, which is time dependent and must wait until sync from t0=0
00177     if (gl_random_uniform(0,1) < 0.8)
00178         h = height;
00179     else
00180         h = 10 * floor(0.1 * gl_random_uniform(0,height));
00181 
00182     // initial water temperature
00183     if (h == 0)
00184 
00185         // discharged
00186         Tw = Tupper = Tlower = Tinlet;  // Note that Tw gets reset, too...
00187     else 
00188     {
00189         Tupper = Tw;
00190         Tlower = Tinlet;
00191     }
00192 
00193     return 1;
00194 }
00195 
00200 void waterheater::thermostat(TIMESTAMP t0, TIMESTAMP t1)
00201 {
00202     /* Update power and internal gain for the current time to synch and set the tank state */
00203     double internal_gain = 0.0;
00204     double nHours = (gl_tohours(t1) - gl_tohours(t0))/TS_SECOND;
00205 
00206     // determine the power used
00207     if (heat_needed == TRUE)
00208         power_kw = actual_Q()/BTUPHPKW;
00209     else
00210         power_kw = 0.0;
00211 
00212     // determine internal gains
00213     if (location == INSIDE)
00214         internal_gain = actual_Q() * nHours;
00215     else
00216         internal_gain = 0;
00217 
00218     // get context of object
00219     OBJECT *parent = (OBJECTHDR(this))->parent;
00220     house *pHouse = OBJECTDATA(parent,house);
00221 
00222     // post internal gains
00223     pHouse->waterheater_heat_energy = internal_gain;
00224 
00225     // calculate temperatures
00226     Ton = tank_setpoint - thermostat_deadband/2;
00227     Toff = tank_setpoint + thermostat_deadband/2;
00228 
00229     // calculate water demand
00230     cur_water_demand = water_demand;
00231     water_demand = last_water_demand;
00232 
00233     // update temperature and height
00234     update_T_and_or_h(nHours);
00235 
00236     // determine tank state
00237     WHQSTATE current_tank_state = tank_state();
00238     switch (current_tank_state) {
00239         case FULL:
00240             if (Tw-TSTAT_PRECISION < Ton)
00241                 heat_needed = TRUE;
00242             else if (Tw+TSTAT_PRECISION > Toff)
00243                 heat_needed = FALSE;
00244             else 
00245             {   // We're in the deadband...leave heat like it was.  
00246             }
00247             break;
00248         case EMPTY:
00249         case PARTIAL:
00250             heat_needed = TRUE;
00251             break;
00252     }
00253 }
00254 
00255 
00259 TIMESTAMP waterheater::sync(TIMESTAMP t0, TIMESTAMP t1) 
00260 {
00261 
00262     // Now find our current temperatures and boundary height...
00263     // And compute the time to the next transition...
00264     water_demand = cur_water_demand;
00265     set_time_to_transition();
00266     last_water_demand = cur_water_demand;
00267 
00268     if (time_to_transition >= 0.0167)   // 0.0167 represents one second
00269         return (TIMESTAMP)(t1+time_to_transition*3600.0/TS_SECOND);
00270     // less than one second means never
00271     else
00272         return TS_NEVER; 
00273 }
00274 
00277 waterheater::WHQSTATE waterheater::tank_state(void)
00278 {
00279     if ( h >= height-HEIGHT_PRECISION )
00280         return FULL;
00281     else if ( h <= HEIGHT_PRECISION)
00282         return EMPTY;
00283     else
00284         return PARTIAL;
00285 }
00286 
00289 void waterheater::set_time_to_transition(void)
00290 {
00291     // set the model and load state
00292     set_current_model_and_load_state();
00293 
00294     time_to_transition = -1;
00295 
00296     switch (current_model) {
00297         case ONENODE:
00298             if (heat_needed == FALSE)
00299                 time_to_transition = new_time_1node(Tw, Ton);
00300             else if (load_state == RECOVERING)
00301                 time_to_transition = new_time_1node(Tw, Toff);
00302             else
00303                 time_to_transition = -1;
00304             break;
00305 
00306         case TWONODE:
00307             switch (load_state) {
00308                 case STABLE:
00309                     time_to_transition = -1; // Negative implies TS_NEVER;
00310                     break;
00311                 case DEPLETING:
00312                     time_to_transition = new_time_2zone(h, 0);
00313                     break;
00314                 case RECOVERING:
00315                     time_to_transition = new_time_2zone(h, height);
00316                     break;
00317             }
00318     }
00319     return;
00320 }
00321 
00326 waterheater::WHQFLOW waterheater::set_current_model_and_load_state(void)
00327 {
00328     double dhdt_now = dhdt(h);
00329     double dhdt_full = dhdt(height);
00330     double dhdt_empty = dhdt(0.0);
00331     current_model = NONE;       // by default set it to onenode
00332     load_state = STABLE;        // by default
00333 
00334     WHQSTATE tank_status = tank_state();
00335 
00336     switch(tank_status) 
00337     {
00338         case EMPTY:
00339             if (dhdt_empty <= 0.0) 
00340             {
00341                 // If the tank is empty, a negative dh/dt means we're still
00342                 // drawing water, so we'll be switching to the 1-zone model...
00343                 current_model = NONE;
00344                 load_state = DEPLETING;
00345             }
00346             else if (dhdt_full > 0)
00347             {
00348                 // overriding the plc code ignoring thermostat logic
00349                 // heating will always be on while in two zone model
00350                 heat_needed = TRUE;
00351                 current_model = TWONODE;
00352                 load_state = RECOVERING;
00353             }
00354             else
00355                 load_state = STABLE;
00356             break;
00357 
00358         case FULL:
00359             // If the tank is full, a negative dh/dt means we're depleting, so
00360             // we'll also be switching to the 2-zone model...
00361             if (dhdt_full < 0)
00362             {
00363                 // overriding the plc code ignoring thermostat logic
00364                 // heating will always be on while in two zone model
00365                 bool cur_heat_needed = heat_needed;
00366                 heat_needed = TRUE;
00367                 double dhdt_full_temp = dhdt(height);
00368                 if (dhdt_full_temp < 0)
00369                 {
00370                     current_model = TWONODE;
00371                     load_state = DEPLETING;
00372                 }
00373                 else
00374                 {
00375                     current_model = ONENODE;
00376                     
00377                     heat_needed = cur_heat_needed;
00378                     load_state = heat_needed ? RECOVERING : DEPLETING;
00379                 }
00380             }
00381             else if (dhdt_empty > 0)
00382             {
00383                 current_model = ONENODE;
00384                 load_state = RECOVERING;
00385             }
00386             else
00387                 load_state = STABLE;
00388             break;
00389 
00390         case PARTIAL:
00391             // We're definitely in 2-zone mode.  We have to watch for the
00392             // case where h's movement stalls out...
00393             current_model = TWONODE;
00394             // overriding the plc code ignoring thermostat logic
00395             // heating will always be on while in two zone model
00396             heat_needed = TRUE;
00397 
00398             if (dhdt_now < 0 && (dhdt_now * dhdt_empty) >= 0)
00399                 load_state = DEPLETING;
00400             else if (dhdt_now > 0 && (dhdt_now * dhdt_full) >= 0) 
00401                 load_state = RECOVERING;
00402             else 
00403             {
00404                 // dhdt_now is 0, so nothing's happening...
00405                 current_model = NONE;
00406                 load_state = STABLE;
00407             }
00408             break;
00409     }
00410 
00411     return load_state;
00412 }
00413 
00414 void waterheater::update_T_and_or_h(double nHours)
00415 {
00416     /*
00417         When this gets called (right after the waterheater gets sync'd),
00418         all states are exactly as they were at the end of the last sync.
00419         We calculate what has happened to the water temperature (or the
00420         warm/cold boundarly location, depending on the current state)
00421         in the interim.  If nHours equals our previously requested
00422         timeToTransition, we should find things landing on a new state.
00423         If not, we should find ourselves in the same state again.  But
00424         this routine doesn't try to figure that out...it just calculates
00425         the new T/h.
00426     */
00427 
00428     // set the model and load state
00429     switch (current_model) 
00430     {
00431         case ONENODE:
00432             // Handy that the 1-node model doesn't care which way
00433             // things are moving (RECOVERING vs DEPLETING)...
00434 SingleZone:
00435             Tw = new_temp_1node(Tw, nHours);
00436             Tupper = Tw;
00437             Tlower = Tinlet;
00438             break;
00439 
00440         case TWONODE:
00441             // overriding the plc code ignoring thermostat logic
00442             // heating will always be on while in two zone model
00443             heat_needed = TRUE;
00444             switch (load_state) 
00445             {
00446                 case STABLE:
00447                     // Change nothing...
00448                     break;
00449                 case DEPLETING:
00450                     // Fall through...
00451                 case RECOVERING:
00452                     try {
00453                         h = new_h_2zone(h, nHours);
00454                     } catch (WRONGMODEL m)
00455                     {
00456                         if (m==MODEL_NOT_2ZONE)
00457                         {
00458                             current_model = ONENODE;
00459                             goto SingleZone;
00460                         }
00461                         else
00462                             GL_THROW("unexpected exception in update_T_and_or_h(%+.1f hrs)", nHours);
00463                     }
00464                     break;
00465             }
00466 
00467             // Correct h if it overshot...
00468             if (h < ROUNDOFF) 
00469             {
00470                 // We've over-depleted the tank slightly.  Make a quickie
00471                 // adjustment to Tlower/Tw to account for it...
00472 
00473                 double vol_over = tank_volume * h/height;  // Negative...
00474                 double energy_over = vol_over * RHOWATER * Cp * (Tupper - Tlower);
00475                 double Tnew = Tlower + energy_over/Cw;
00476                 Tw = Tlower = Tnew;
00477                 h = 0;
00478             } 
00479             else if (h > height) 
00480             {
00481                 // Ditto for over-recovery...
00482                 double vol_over = tank_volume * (h-height)/height;
00483                 double energy_over = vol_over * RHOWATER * Cp * (Tupper - Tlower);
00484                 double Tnew = Tupper + energy_over/Cw;
00485                 Tw = Tupper = Tnew;
00486                 Tlower = Tinlet;
00487                 h = height;
00488             } 
00489             else 
00490             {
00491                 // Note that as long as h stays between 0 and height, we don't
00492                 // adjust Tlower, even if the Tinlet has changed.  This avoids
00493                 // the headache of adjusting h and is of minimal consequence because
00494                 // Tinlet changes so slowly...
00495                 Tupper = Tw;
00496             }
00497             break;
00498 
00499         default:
00500             break;
00501     }
00502 
00503     return;
00504 }
00505 
00506 double waterheater::dhdt(double h)
00507 {
00508     if (Tupper - Tlower < ROUNDOFF)
00509         return 0.0; // if Tupper and Tlower are same then dh/dt = 0.0;
00510 
00511     // Pre-set some algebra just for efficiency...
00512     const double mdot = water_demand * 60 * RHOWATER / GALPCF;      // lbm/hr...
00513     const double c1 = RHOWATER * Cp * area * (Tupper - Tlower);
00514     
00515     // check c1 before dividing by it
00516     if (c1 <= ROUNDOFF)
00517         return 0.0; //Possible only when Tupper and Tlower are very close, and the difference is negligible
00518 
00519     const double cA = -mdot / (RHOWATER * area) + (actual_Q() + tank_UA * (get_Tambient(location) - Tlower)) / c1;
00520     const double cb = (tank_UA / height) * (Tupper - Tlower) / c1;
00521 
00522     // Returns the rate of change of 'h'
00523     return cA + cb*h;
00524 }
00525 
00526 double waterheater::actual_Q(void)
00527 {
00528     const double nominal_voltage = 240.0; //@TODO:  Determine if this should be published or how we want to obtain this from the equipment/network
00529     static int trip_counter = 0;
00530 
00531     // calculate rated heat capacity adjusted for the current line voltage
00532     if (heat_needed)
00533     {
00534         if (pVoltage->Mag() > 2.0*nominal_voltage)
00535         {
00536             if (trip_counter++ > 10)
00537                 GL_THROW("Water heater line voltage is too high, exceeds twice nominal voltage.");
00538             else
00539                 return 0.0;         // @TODO:  This condition should trip the breaker with a counter
00540         }
00541 
00542         return heating_element_capacity * (pow(pVoltage->Mag(), 2.0) / (pow (nominal_voltage, 2.0)));
00543     }
00544     else
00545         return 0.0;
00546 }
00547 
00548 inline double waterheater::new_time_1node(double T0, double T1)
00549 {
00550     const double mdot_Cp = Cp * water_demand * 60 * RHOWATER / GALPCF;
00551 
00552     if (Cw <= ROUNDOFF)
00553         return -1.0;
00554 
00555     const double c1 = ((actual_Q() + tank_UA * get_Tambient(location)) + mdot_Cp*Tinlet) / Cw;
00556     const double c2 = -(tank_UA + mdot_Cp) / Cw;
00557 
00558     if (fabs(c1 + c2*T1) <= ROUNDOFF || fabs(c1 + c2*T0) <= ROUNDOFF || fabs(c2) <= ROUNDOFF)
00559         return -1.0;
00560 
00561     const double new_time = (log(fabs(c1 + c2 * T1)) - log(fabs(c1 + c2 * T0))) / c2;   // [hr]
00562     return new_time;
00563 }
00564 
00565 inline double waterheater::new_temp_1node(double T0, double delta_t)
00566 {
00567     const double mdot_Cp = Cp * water_demand * 60 * RHOWATER / GALPCF;
00568 
00569     if (Cw <= ROUNDOFF || (tank_UA+mdot_Cp) <= ROUNDOFF)
00570         return T0;
00571 
00572     const double c1 = (tank_UA + mdot_Cp) / Cw;
00573     const double c2 = (actual_Q() + mdot_Cp*Tinlet + tank_UA*get_Tambient(location)) / (tank_UA + mdot_Cp);
00574 
00575     return  c2 - (c2 - T0) * exp(-c1 * delta_t);    // [F]
00576 }
00577 
00578 
00579 inline double waterheater::new_time_2zone(double h0, double h1)
00580 {
00581     const double c0 = RHOWATER * Cp * area * (Tupper - Tlower);
00582 
00583     if (fabs(c0) <= ROUNDOFF || height <= ROUNDOFF)
00584         return -1.0;    // c0 or height should never be zero.  if one of these is zero, there is no definite time to transition
00585 
00586     const double cb = (tank_UA / height) * (Tupper - Tlower) / c0;
00587 
00588     if (fabs(cb) <= ROUNDOFF)
00589         return -1.0;
00590 
00591     return (log(fabs(dhdt(h1))) - log(fabs(dhdt(h0)))) / cb;    // [hr]
00592 }
00593 
00594 inline double waterheater::new_h_2zone(double h0, double delta_t)
00595 {
00596     if (delta_t <= ROUNDOFF)
00597         return h0;
00598 
00599     const double mdot = water_demand * 60 * RHOWATER / GALPCF;      // lbm/hr...
00600     const double c1 = RHOWATER * Cp * area * (Tupper - Tlower);
00601 
00602     // check c1 before division
00603     if (fabs(c1) <= ROUNDOFF)
00604         return height;      // if Tupper and Tlower are real close, then the new height is the same as tank height
00605 //      throw MODEL_NOT_2ZONE;
00606         
00607 
00608     const double cA = -mdot / (RHOWATER * area) + (actual_Q() + tank_UA * (get_Tambient(location) - Tlower)) / c1;
00609     const double cb = (tank_UA / height) * (Tupper - Tlower) / c1;
00610 
00611     if (fabs(cb) <= ROUNDOFF)
00612         return height;
00613 
00614     return ((exp(cb * delta_t) * (cA + cb * h0)) - cA) / cb;    // [ft]
00615 }
00616 
00617 double waterheater::get_Tambient(WHLOCATION loc)
00618 {
00619     double ratio;
00620 
00621     switch (loc) {
00622     case GARAGE: // temperature is about 1/2 way between indoor and outdoor
00623         ratio = 0.5;
00624         break;
00625     case INSIDE: // temperature is all indoor
00626     default:
00627         ratio = 1.0;
00628         break;
00629     }
00630 
00631     // return temperature of location
00632     return pHouse->get_Tair()*ratio + pHouse->get_Tout()*(1-ratio);
00633 }
00634 
00635 void waterheater::wrong_model(WRONGMODEL msg)
00636 {
00637     char *errtxt[] = {"model is not one-zone","model is not two-zone"};
00638     OBJECT *obj = OBJECTHDR(this);
00639     gl_warning("%s (waterheater:%d): %s", obj->name?obj->name:"(anonymous object)", obj->id, errtxt[msg]);
00640     throw msg; // this must be caught by the waterheater code, not by the core
00641 }
00642 
00644 // IMPLEMENTATION OF CORE LINKAGE
00646 
00647 EXPORT int create_waterheater(OBJECT **obj, OBJECT *parent)
00648 {
00649     *obj = gl_create_object(waterheater::oclass,sizeof(waterheater));
00650     if (*obj!=NULL)
00651     {
00652         waterheater *my = OBJECTDATA(*obj,waterheater);;
00653         gl_set_parent(*obj,parent);
00654         my->create();
00655         return 1;
00656     }
00657     return 0;
00658 }
00659 
00660 EXPORT int init_waterheater(OBJECT *obj)
00661 {
00662     waterheater *my = OBJECTDATA(obj,waterheater);
00663     return my->init(obj->parent);
00664 }
00665 
00666 EXPORT TIMESTAMP sync_waterheater(OBJECT *obj, TIMESTAMP t0)
00667 {
00668     waterheater *my = OBJECTDATA(obj, waterheater);
00669     if (obj->clock <= ROUNDOFF)
00670         obj->clock = t0;  //set the object clock if it has not been set yet
00671 
00672     try {
00673         TIMESTAMP t1 = my->sync(obj->clock, t0);
00674         obj->clock = t0;
00675         return t1;
00676     }
00677     catch (int m)
00678     {
00679         gl_error("%s (waterheater:%d) model zone exception (code %d) not caught", obj->name?obj->name:"(anonymous waterheater)", obj->id, m);
00680     }
00681     catch (char *msg)
00682     {
00683         gl_error("%s (waterheater:%d) %s", obj->name?obj->name:"(anonymous waterheater)", obj->id, msg);
00684     }
00685     return TS_INVALID;
00686 }
00687 
00688 EXPORT TIMESTAMP plc_waterheater(OBJECT *obj, TIMESTAMP t0)
00689 {
00690     // this will be disabled if a PLC object is attached to the waterheater
00691     if (obj->clock <= ROUNDOFF)
00692         obj->clock = t0;  //set the clock if it has not been set yet
00693 
00694     waterheater *my = OBJECTDATA(obj,waterheater);
00695     my->thermostat(obj->clock, t0);
00696     
00697     // no changes to timestamp will be made by the internal water heater thermostat
00699     return TS_NEVER;  
00700 }
00701 

GridLAB-DTM Version 1.0
An open-source project initiated by the US Department of Energy