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
00035 CLASS* waterheater::oclass = NULL;
00036 waterheater *waterheater::defaults = NULL;
00037
00040 waterheater::waterheater(MODULE *module)
00041 {
00042
00043 if (oclass==NULL)
00044 {
00045
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
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
00071 defaults = this;
00072
00073
00074 tank_volume = 50.0;
00075 tank_UA = 0.0;
00076 tank_diameter = 1.5;
00077 Tinlet = 60.0;
00078 water_demand = 0.0;
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
00096 memcpy(this,defaults,sizeof(*this));
00097
00098
00099 location = gl_random_bernoulli(0.80) ? GARAGE : INSIDE;
00100
00101
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
00119 pVoltage = (pHouse->attach(OBJECTHDR(this),30,TRUE))->pV;
00120
00121
00122
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));
00125
00126
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
00133 if (tank_UA <= 0.0)
00134
00135
00136 tank_UA = clip(gl_random_normal(2.0, 0.20),0.1,10) * tank_volume/50;
00137
00138
00139 if (heating_element_capacity <= 0.0)
00140 {
00141 if (tank_volume >= 50)
00142 heating_element_capacity = 4500;
00143 else
00144 {
00145
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
00157 Tw = 122.0;
00158 current_model = NONE;
00159 load_state = STABLE;
00160
00161
00162 Tset_curtail = tank_setpoint - thermostat_deadband/2 - 10;
00163
00164
00165 heating_element_capacity *= BTUPHPW;
00166 tank_volume /= GALPCF;
00167
00168
00169 area = (pi * pow(tank_diameter,2))/4;
00170 height = tank_volume / area;
00171 Cw = tank_volume * RHOWATER * Cp;
00172
00173
00174 cur_water_demand = last_water_demand = water_demand;
00175
00176
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
00183 if (h == 0)
00184
00185
00186 Tw = Tupper = Tlower = Tinlet;
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
00203 double internal_gain = 0.0;
00204 double nHours = (gl_tohours(t1) - gl_tohours(t0))/TS_SECOND;
00205
00206
00207 if (heat_needed == TRUE)
00208 power_kw = actual_Q()/BTUPHPKW;
00209 else
00210 power_kw = 0.0;
00211
00212
00213 if (location == INSIDE)
00214 internal_gain = actual_Q() * nHours;
00215 else
00216 internal_gain = 0;
00217
00218
00219 OBJECT *parent = (OBJECTHDR(this))->parent;
00220 house *pHouse = OBJECTDATA(parent,house);
00221
00222
00223 pHouse->waterheater_heat_energy = internal_gain;
00224
00225
00226 Ton = tank_setpoint - thermostat_deadband/2;
00227 Toff = tank_setpoint + thermostat_deadband/2;
00228
00229
00230 cur_water_demand = water_demand;
00231 water_demand = last_water_demand;
00232
00233
00234 update_T_and_or_h(nHours);
00235
00236
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 {
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
00263
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)
00269 return (TIMESTAMP)(t1+time_to_transition*3600.0/TS_SECOND);
00270
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
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;
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;
00332 load_state = STABLE;
00333
00334 WHQSTATE tank_status = tank_state();
00335
00336 switch(tank_status)
00337 {
00338 case EMPTY:
00339 if (dhdt_empty <= 0.0)
00340 {
00341
00342
00343 current_model = NONE;
00344 load_state = DEPLETING;
00345 }
00346 else if (dhdt_full > 0)
00347 {
00348
00349
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
00360
00361 if (dhdt_full < 0)
00362 {
00363
00364
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
00392
00393 current_model = TWONODE;
00394
00395
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
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
00418
00419
00420
00421
00422
00423
00424
00425
00426
00427
00428
00429 switch (current_model)
00430 {
00431 case ONENODE:
00432
00433
00434 SingleZone:
00435 Tw = new_temp_1node(Tw, nHours);
00436 Tupper = Tw;
00437 Tlower = Tinlet;
00438 break;
00439
00440 case TWONODE:
00441
00442
00443 heat_needed = TRUE;
00444 switch (load_state)
00445 {
00446 case STABLE:
00447
00448 break;
00449 case DEPLETING:
00450
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
00468 if (h < ROUNDOFF)
00469 {
00470
00471
00472
00473 double vol_over = tank_volume * h/height;
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
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
00492
00493
00494
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;
00510
00511
00512 const double mdot = water_demand * 60 * RHOWATER / GALPCF;
00513 const double c1 = RHOWATER * Cp * area * (Tupper - Tlower);
00514
00515
00516 if (c1 <= ROUNDOFF)
00517 return 0.0;
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
00523 return cA + cb*h;
00524 }
00525
00526 double waterheater::actual_Q(void)
00527 {
00528 const double nominal_voltage = 240.0;
00529 static int trip_counter = 0;
00530
00531
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;
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;
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);
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;
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;
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;
00600 const double c1 = RHOWATER * Cp * area * (Tupper - Tlower);
00601
00602
00603 if (fabs(c1) <= ROUNDOFF)
00604 return height;
00605
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;
00615 }
00616
00617 double waterheater::get_Tambient(WHLOCATION loc)
00618 {
00619 double ratio;
00620
00621 switch (loc) {
00622 case GARAGE:
00623 ratio = 0.5;
00624 break;
00625 case INSIDE:
00626 default:
00627 ratio = 1.0;
00628 break;
00629 }
00630
00631
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;
00641 }
00642
00644
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;
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
00691 if (obj->clock <= ROUNDOFF)
00692 obj->clock = t0;
00693
00694 waterheater *my = OBJECTDATA(obj,waterheater);
00695 my->thermostat(obj->clock, t0);
00696
00697
00699 return TS_NEVER;
00700 }
00701