Kirjoittaja Aihe: lämminvesivaraajan tehonsäätö  (Luettu 10857 kertaa)

kotte

  • Aktiivijäsen
  • ***
  • Viestejä: 1044
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #45 : 01.11.18 - klo:13:38 »
Tuo dimmer vaikuttaisi olevan juuri se joka sopisi minun tarkoitukseeni.
Katselin vain eBay:n kuvasta että miten tuo voi kestää 1kw tehon virtaa kuumenematta liikaa, ei oiken  mitään jäähdytystä?
Jos tuo kestää 12A / 220V (mikä muuten ei periaatteessa ihan riitä Suomessa jännitteen osalta), niin laitehan pystyy ohjaamaan tehoa yli 2,6kW:n edestä. Jos virta on 12A ja kyseessä olevan ohjauskomponentin jännitehäviöksi oletaan n. 1,5V, jää hukkatehon tuotoksi alle 20W, mutta jonkinlaisen kunnollisen jäähdytyssiilin tuo silti vaatisi. Taitaa tuo 12A olla venttiilikomponentin maksimivirta ja ongelmia tulee alta aikayksikön, jos yrittää ohjata enemmän kuin muutaman sadan watin kulutuslaitetta. Tuollaisia tehojahan valokytkimen paikalla tarkoitetut vastaavantasoisesti jäähdytetyt himmenninkojeet tyypillisesti korkeintaan kestävät.

Tuolta linkin sivulta muuten löytyy linkki dokumenttiin, jossa on useampikanavaisiakin vaihekulmasäätimiä. Noissa näkyy jo olevan ihan asiallisia jäähdytyssiilejä.

Toisaalta vastaavista vaihekulmasäätimistä on paljonkin valmista tarjontaa erilaisilla ohjaustavoilla, niin analogisilla kuin digitaalisillakin.

pere

  • Jäsen
  • **
  • Viestejä: 56
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #46 : 02.11.18 - klo:11:44 »
Jollalle kysymys.
Aikaisemmista viesteistäsi ja "korsteeni" kuvista päättelin että sinulla on säädössä SSR.
Kun nyt täällä ollaan sitämieltä että se ei oikein toimi niin miten sinulla toimii?
Vai olenko käsittänyt väärin?
Jotenkin tuo tehon ohjaaminen ssr/pwm tuntuu "tyylikkäämmältä"!

jolla

  • Aktiivijäsen
  • ***
  • Viestejä: 575
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #47 : 02.11.18 - klo:14:19 »
Jollalle kysymys.
Aikaisemmista viesteistäsi ja "korsteeni" kuvista päättelin että sinulla on säädössä SSR.
Kun nyt täällä ollaan sitämieltä että se ei oikein toimi niin miten sinulla toimii?
Vai olenko käsittänyt väärin?
Jotenkin tuo tehon ohjaaminen ssr/pwm tuntuu "tyylikkäämmältä"!

kyllä se toimii SSR/pwm, ongelmia oli saada tarpeeksi stabilia ja luotettavaa ohjausdataa ja yli 2kW alkoivat tuottaa ongelmia kuten myös tällä mk2pv'llä mikä nyt on käytössä
ongelma ratkesi toisella arduinolla mikä ei tee muuta kun kytkee SSR kanssa lisäkuormaa kiinetästi tarvittaessa, uskoisin että ssr/pwm toimisi myös tällä logiikalla

nyt on käytössä linkin http://www.elisanet.fi/korsteeni/mk2pv.shtml toiseksi alimman kuvan mukainen kytkentä millä toimii moitteetta 1,6kW vastuksen kanssa ja lisäkuormilla (se toinen arduino) 7kW saakka on toimiminut saumattomasti

pwm'ällä eikä pulssilla yli 2kW kuomilla ei saa toimimaan näillä linjoilla tässä taloudessa

ihan rautalankana
- kun tuotto ( panelien tuotto - kulutus) on 0-1,6 kW mk2pv hoitaa säädön 1,6kw vastuksella
- yli 1,6kW toinen arduino kytkee 1,6 kw vastuksen kiinteästi
- yli 3,2 kw kytkee 2,5kw ja 1,6kW irti
- jne

Koodia: [Valitse]
// EmonLibrary examples openenergymonitor.org, Licence GNU GPL V3

#include "EmonLib.h"             // Include Emon Library
EnergyMonitor emon1;             // Create an instance
int upPin7 = 7;
int upPin8 = 8;
int upPin9 = 9;

void setup()
{
  pinMode(upPin7,OUTPUT);
  pinMode(upPin8,OUTPUT);
  pinMode(upPin9,OUTPUT); 
  Serial.begin(9600);
 
  emon1.voltage(2, 150, 1.7);  // Voltage: input pin, calibration, phase_shift
  emon1.current(1, 12.7);       // Current: input pin, calibration.
}

void loop()
{
  emon1.calcVI(20,2000);         // Calculate all. No.of half wavelengths (crossings), time-out
  emon1.serialprint();           // Print out all variables (realpower, apparent power, Vrms, Irms, power factor)
 
  float realPower       = emon1.realPower;        //extract Real Power into variable
  float apparentPower   = emon1.apparentPower;    //extract Apparent Power into variable
  float powerFActor     = emon1.powerFactor;      //extract Power Factor into Variable
  float supplyVoltage   = emon1.Vrms;             //extract Vrms into Variable
  float Irms            = emon1.Irms;             //extract Irms into Variable
 
    if ((realPower >= 1600) && (realPower < 3200)) //170W tullut yli 28 p�iv�n�
    {
    digitalWrite(upPin7, HIGH);
    digitalWrite(upPin8, LOW);
    digitalWrite(upPin9, LOW);
    }
    else if ((realPower >= 3200) && (realPower < 4100))
    {
    digitalWrite(upPin8, HIGH);
    digitalWrite(upPin7, LOW);
    digitalWrite(upPin9, LOW);
    }
    else if ((realPower >= 4100) && (realPower < 5400))
    {
    digitalWrite(upPin8, HIGH);
    digitalWrite(upPin7, HIGH);
    digitalWrite(upPin9, LOW);
    }
    else if ((realPower >= 5400) && (realPower < 8200))
    {
    digitalWrite(upPin8, HIGH);
    digitalWrite(upPin9, HIGH);
    digitalWrite(upPin7, LOW);
    }
    else
    {
    digitalWrite(upPin7, LOW);
    digitalWrite(upPin8, LOW);
    digitalWrite(upPin9, LOW);
    } 
}


käytössä oleva mk2pv koodi

Koodia: [Valitse]
// Mk2a is a revised version of my stand-alone sketch for diverting suplus
// PV power to a dump load using a triac.  The original Mk2 version, which includes
// more explanation and 'debug' code to support off-line working, may be found at
// http://openenergymonitor.org/emon/node/841
//
// General aspects of Mk2a:
// The Mk2a code is suitable for hardware that has multiple voltage references, such
// as emonTx.  Mk2a uses integer maths for improved speed.  It also uses low-level
// instructions for ADC operations which 'frees up' time that was previously unavailable
// for general processing activities.  Further information about this enhancement may be
// found immediately before loop().
//
// Specific releases of Mk2a:
// The initial release of Mk2a had twin high-pass filters, as used in the standard OEM
// V&I sketch.  Due to a couple of minor bugs, it was soon replaced by the _rev2 version.
//
// The _rev3 version marks a further change to the way that the raw sample streams are
// processed.  This has come about because, for the purpose of calculating "real power",
// there is no need for DC offset to be pre-removed from the raw current samples. The
// standard calculation for real power does this automatically when applied over each whole
// cycle of the mains.  Half-cycle processing of power has been discontinued.
//   A single LPF, which is updated just once per mains cycle, has been introduced for
// the purpose of determining the DC offset of the voltage waveform.  This value is then
// subtracted from each raw voltage sample.  A nominal value of 512 is subtracted from
// each current sample, this being to prevent the system becoming over-sensitive to any
// imbalance conditions or random noise; the actual value is unimportant.
//
// The ANTI_FLICKER option was also introduced in _rev3.  This mode uses two thresholds
// for the energy bucket rather than just one.  When surplus power is available,
// the power-diversion logic operates the triac between these two limits on a hysteresis
// basis.  To ensure that the anti-flicker requirement is always met, a minimum amount of
// time has to elapse between consecutive activations of the triac.  Under certain
// conditions, this may cause a small amount of surplus power to be lost when the bucket
// overflows.  The on-board LED (pin 13) shows whenever then this occurs. Accurate
// calibration of the system is essential when operating in this mode.
// 
// TALLYMODE is a #define option which allows energy data to be collected for
// subsequent display.  In _rev2, energy values in positive and negative half-cycles was
// recorded separately.  From _rev3 onwards, energy is recorded for whole cycles only.
//   While running in TALLYMODE, the code is fully functional.  It pauses after a
// user-selectable period so that stored data can be retrieved.  To avoid overflow, the
// maximum recommended duration is 10 minutes.  To start again, press the 'g' key, then
// carriage-return.
//   TALLYMODE provides a very easy way to calibrate the system.  Clip the CT around
// one core of a cable that supplies a known load, and do a short run with appropriate
// Min and Max values.  If the peak distribution (in Watts) is not where you expect it
// to be, then just change the value of powerCal until it is. 
//   Full details about calibration are provided just before setup().
//
// SPEEDCHECK is another #define option.  When #included, the results of a built-in
// checker facility are displayed.  This facility keeps track of the number of times
// that general processing fails to complete within the duration of the associated
// ADC conversion.  SPEEDCHECK may be left on permanently, if desired.
//   If additional workload is ever to be added, SPEEDCHECK will provide a sensitive
// indication of any undesirable effect on the underlying code.  Any value other than
// zero will reduce the accuracy of power/energy calculations.
//
// Mk2a_rev3a.  This corrects a minor but long-standing error in the data produced by
// Tallymode.  Tallymode data is now displayed in Watts, rather than in Tally numbers.
//
//                  Robin Emley (calypso_rae on Open Energy Monitor Forum)
//                  June 2013


/*
Circuit for monitoring pulses from a supply meter using checkLedStatus():
 
                  ----------------> +5V
                  |
                  /
                  \ 8K2
                  /
                  |
             ---------------------> dig 2
             |       |
        -->  /       |
        -->  \       _
        LDR  /       -  0.01uF
             |       |
             ----------------------> GND
*/

// There are two #define options which may be activiated independently
//
//#define TALLYMODE // for recording energy data.  Comment out for normal operation
//#define SPEEDCHECK // to display the results of a built-in checker every few seconds

enum operatingModes {NORMAL, ANTI_FLICKER};
enum operatingModes operatingMode = NORMAL; // <-- select the desired mode here

#define CYCLES_PER_SECOND 50
#define JOULES_PER_WATT_HOUR 3600 // 0.001 kWh = 3600 Joules
enum polarities {NEGATIVE, POSITIVE};
enum triacStates {ON, OFF}; // the external trigger device is active low

// general global variables
const byte outputPinForLed = 13;  // digital
const byte outputPinForTrigger = 9; // digital
const byte ledDetectorPin = 2;  // digital
const byte ledRepeaterPin = 10;  // digital
const byte voltageSensorPin = 2;  // analogue
const byte currentSensorPin = 1;   // analogue
const byte startUpPeriod = 5; // in seconds, to allow HP filters to settle
const int DCoffset_I = 512; // for calculating "real power", a nominal value is fine

long cycleCount = 0;
long cycleCountAtLastActivation = 0;
int samplesDuringThisCycle;
enum triacStates nextStateOfTriac;
enum triacStates triacState;
boolean triggerNeedsToBeArmed;
boolean beyondStartUpPhase = false;
float safetyMargin_WattsPerHalfCycle;
long triggerThreshold_long;
long energyInBucket_long = 0; // for integer maths
int phaseCal_int; // for integer maths
long capacityOfEnergyBucket_long;  // for integer maths
long energyThreshold_long;  // for integer maths
long antiFlicker_lowerEnergyThreshold;
long antiFlicker_upperEnergyThreshold;
long sumP;// for integer maths
long DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
long DCoffset_V_min;
long DCoffset_V_max;

float minTimeBetweenActivations = 3; // <-- anti-flicker requirement (seconds)
int minCycleCountsBetweenActivations;

// items to allow general processing tasks to be efficiently partitioned.
boolean newHalfCycleFlag;
boolean voltageOKToArmTriggerFlag;
enum polarities polarityNow;
long instP;
long sampleVminusDC_long;

// values that need to be stored from one loop to the next
long lastSampleVminusDC_long;  //    for phaseCal algorithm
enum polarities polarityOfLastSampleV; // for zero-crossing detection
long cumVdeltasThisCycle_long;   // <<--- for LPF
int sampleV_forNextLoop; // for deferred processing
int sampleI_forNextLoop; // for deferred processing

// items for LED monitoring
byte ledState, prevLedState;
boolean ledRecentlyOnFlag = false;
unsigned long ledOnAt;
long energyInBucket_4led_long = 0;

// for the in-built mechanism to check whether the ADC conversion time is ever exceeded
boolean speedFlag;
long FlagNotClearedCounter = 0;
long loopCountForSpeedChecker = 0;
long maxLoopCountForSpeedChecker = 20000; // how often to display results (~ 4500 loops/sec)

#ifdef TALLYMODE
#define NUMBER_OF_TALLIES 100
unsigned int tally[NUMBER_OF_TALLIES + 2]; // For recording the energy content in whole mains cycles.
unsigned int tallymode_maxCycleCount;  // the cycleCount value when recording should cease
int tallymode_maxVal; // the maximum power to be recorded (Watts)
int tallymode_minVal; // the minimum power to be recorded (Watts)
//long tallymode_minVal_long; // x256 version for integer maths
float tallymode_stepVal; // the power increment between consecutive tallies (Watts)
//long tallymode_stepVal_long; // x256 version for integer maths
boolean tallymode_firstLoop = true;
unsigned int tallymode_noOfValuesTallied; // overflows after 10.9 minutes
unsigned long tallymode_noOfSamplePairs; // to show the average samples-per-mains-cycle rate
int tallymode_durationOfRecording; // in seconds
#endif

// Calibration values
//-------------------
// Three calibration values are required: powerCal, voltageCal and phaseCal.
// With most hardware, the default values are likely to work fine without
// need for change.  A full explanation of each of these values now follows:
//   
// powerCal is a floating point variable which is used for converting the
// product of voltage and current samples into Watts.
//
// The correct value of powerCal is entirely dependent on the hardware that is
// in use.  For best resolution, the hardware should be configured so that the
// voltage and current waveforms each span most of the ADC's usable range.  For
// many systems, the maximum power that will need to be measured is around 3kW.
//
// My sketch "MinAndMaxValues.ino" provides a good starting point ideal for
// system setup.  First arrange for the CT to be clipped around either core of a 
// cable which supplies a suitable load; then run the tool.  The resulting values
// should sit nicely within the range 0-1023.  To allow some room for safety,
// a margin of around 100 levels should be left at either end.  This gives a
// output range of around 800 ADC levels, which is 80% of its usable range.
//
// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the
// voltage and current waveforms.  This provides an easy way for the user to be
// confident that their system has been set up correctly for the power levels
// that are to be measured.
//
// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
// The purpose of each input sensor is to convert the measured parameter into a
// low-voltage signal which fits nicely within the ADC's input range.
//
// In the case of 240V mains voltage, the numerical value of the input signal
// in Volts is likely to be fairly similar to the output signal in ADC levels. 
// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal
// output range.  Stated more formally, the conversion rate of the overall system
// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
//
// In the case of AC current, however, the situation is very different.  At
// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
// has a peak-to-peak range of 35A.  This is smaller than the output signal by
// around a factor of twenty.  The conversion rate of the overall system for
// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
//
// When measuring power, which is what this code does, the individual
// conversion rates for voltage and current are not of importance.  It is
// only the conversion rate for POWER which is important.  This is the
// product of the individual conversion rates for voltage and current.  It
// therefore has the units of ADC-steps squared per Watt.  Most systems will
// have a power conversion rate of around 20 (ADC-steps squared per Watt), so
// is a good default value to start with.
//
// powerCal is the RECIPR0CAL of the power conversion rate.  A good value
// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
//
const float powerCal = 0.067;  // <---- powerCal value
 
// As mentioned above, the conversion rate for AC voltage has units of 
// ADC-steps per Volt.  Athough not required for measuring power, this
// conversion rate does need to be known in order to determine when the
// voltage level is suitable for arming the external trigger device.
//
// To determine the voltage conversion rate, note the Min and Max values that
// are seen when measuring 240Vac via the voltage sensor.  Subtract one from
// the other to find the range.  Then put that value into the voltageCal formula
// below.  voltageCal has units of ADC-steps per Volt.
//
// 679 is the peak-to-peak range of a 240V RMS signal.  The (float) typecast
// is to prevent integer rounding
//
const float voltageCal = 656 / (float)679; // <-- the first number is the output range of
                               //     your ADC when 240V AC is being measured
                       
// phaseCal is used to alter the phase of the voltage waveform relative to the
// current waveform.  The algorithm interpolates between the most recent pair
// of voltage samples according to the value of phaseCal.
//
//    With phaseCal = 1, the most recent sample is used. 
//    With phaseCal = 0, the previous sample is used
//    With phaseCal = 0.5, the mid-point (average) value in used
//
// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
// and are not recommended.  By altering the order in which V and I samples are
// taken, and for how many loops they are stored, it should always be possible to
// arrange for the optimal value of phaseCal to lie within the range 0 to 1.  When
// measuring a resistive load, the voltage and current waveforms should be perfectly
// aligned.  In this situation, the Power Factor will be 1.
//
// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct
// value of phaseCal for any hardware configuration. 
//
const float  phaseCal = 1.0;


void setup()

  Serial.begin(9600);
  pinMode(outputPinForTrigger, OUTPUT); 
  pinMode(outputPinForLed, OUTPUT); 
  Serial.println();
  Serial.println("starting new run ...");
  Serial.println(); 
  Serial.print("DCoffset_V_long = "); 
  Serial.println(DCoffset_V_long); 
 
   
  // When using integer maths, calibration values that have supplied in floating point
  // form need to be rescaled. 
  //
  phaseCal_int = phaseCal * 256; // for integer maths
 
  // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
  // voltage conversion rate that is in use.  This avoids the need to re-scale every
  // energy contribution, thus saving processing time.  To avoid integer rounding,
  // energy is also recorded at x50 scale. 
  //   This process is described in more detail in the function, processAllOtherTasks(),
  // just before the bucket is updated at the start of each new whole-cycle of the mains.
  // 
  capacityOfEnergyBucket_long = (JOULES_PER_WATT_HOUR * 50L) * (1/powerCal);
 
  Serial.print("powerCal = ");
  Serial.print(powerCal,4);
  Serial.println(" Watts per ADCstep^2");
  Serial.print("energy bucket = 3600 * 50 * (1/");
  Serial.print(powerCal,4);
  Serial.print(") = ");
  Serial.print(capacityOfEnergyBucket_long);
  Serial.println(" energy measurement units");
  Serial.println(); 
                                               
  energyThreshold_long = capacityOfEnergyBucket_long * 0.5; // for normal operation
  antiFlicker_lowerEnergyThreshold = capacityOfEnergyBucket_long * 0.1; // for anti-flicker mode
  antiFlicker_upperEnergyThreshold = capacityOfEnergyBucket_long * 0.9; // for anti-flicker mode 
  minCycleCountsBetweenActivations =
       minTimeBetweenActivations * 50; // cycleCount increments every 20mS

  if (operatingMode == NORMAL) {
    energyInBucket_long = 0.8 * energyThreshold_long; } // for faster start-up 
 
  triggerThreshold_long = 50 * 256L / voltageCal;
    // +50V is a suitable point in the rising waveform to arm the zero-crossing trigger.
    // The 256 is because filteredV is scaled at x256 when integer maths is used.
    // The reciprocal of voltageCal converts ADClevels into Volts, as described above.
   
  DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
  DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
}

// --------------------------------------------------------------------------------------
// Each time around loop(), a new pair of V & I samples is taken.  Rather than using the
// normal analogRead() statement, the ADC is instructed using low-level instructions. 
// By this means, the ADC's conversion time can be used to process data that was collected
// during the >>PREVIOUS<< iteration of loop(). 
//
// Thanks to the use of integer maths, general processing has been greatly speeded up.
// General processing has also been split into two sections, each of which can fit nicely
// into one of the periods while ADC conversions are taking place. The ADC pre-processor
// is now spending all of its time doing back-to-back V and I conversions, and the main
// Arduino is comfortably able to do all of its processing activities during these
// 'hidden' ADC conversion periods.  The amount of delay that can be attributed to
// general processing activities is now ...  NIL  :-)
//
// The first ADC conversion is for current, that's because the phase of this waveform is
// generally slightly advanced relative to the voltage.  While this first ADC conversion
// is taking place, all of the standard per-loop processing activities are done.
//
// The second ADC conversion is for voltage, that's because this waveform is generally
// slightly retarded relative to the current.  While this second ADC conversion is taking
// place, all of the other general processing activities are done.  This includes the
// updating of the energy bucket, which now occurs twice per mains cycle, and also the
// arming of the zero-crossing trigger.
//
// While restructuring the general processing code, some additional flags have been devised
// so that the overall workload can be partitioned more effectively.  Some variables that
// were previously defined as 'locals' within loop() have now become globals.
//
void loop()             
{
  speedFlag = true;
  ADMUX = 0x40 + currentSensorPin;
  ADCSRA |= (1<<ADSC); // instruct ADC to read the sensor for CURRENT
  {
    processPerLoopTasks(); // all general per-loop processing
    delayMicroseconds(10); // for speed checks only
  }
  while(bit_is_set(ADCSRA, ADSC)) {speedFlag = false;};  // wait for ADC to complete
  sampleI_forNextLoop = ADC;
  FlagNotClearedCounter+= speedFlag; // if the ADC conversion time has been exceeded, it's noted
 
  speedFlag = true;
  ADMUX = 0x40 + voltageSensorPin;
  ADCSRA |= (1<<ADSC); // instruct ADC to read the sensor for VOLTAGE
  {
    processAllOtherTasks(); // all other processing activities
    delayMicroseconds(10); // for speed checks only
  }
  while(bit_is_set(ADCSRA, ADSC)) {speedFlag = false;};  // wait for ADC to complete
  sampleV_forNextLoop = ADC;
  FlagNotClearedCounter+= speedFlag; // if the ADC conversion time has been exceeded, it's noted
 
#ifdef SPEEDCHECK  // this is only processed if needed
  loopCountForSpeedChecker++;
  if (loopCountForSpeedChecker == maxLoopCountForSpeedChecker)
  {
    Serial.println(FlagNotClearedCounter);
    loopCountForSpeedChecker = 0;
    FlagNotClearedCounter = 0;
  }
#endif
} // end of loop()


void processPerLoopTasks()
{
  // all tasks that are required for each loop have been grouped into this routine
#ifdef TALLYMODE
  tallymode_checks();
#endif

  int sampleI = sampleI_forNextLoop; // extract samples taken on previous loop.  This is for clarity
  int sampleV = sampleV_forNextLoop; // only; the stored values won't change during this routine.
 
  // remove DC offset from the raw voltage sample by subtracting the accurate value as determined by a LP filter.
  sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long;

  // remove most of the DC offset from the current sample (the precise value does not matter)
  long sampleIminusDC_long = ((long)(sampleI-DCoffset_I))<<8;
 
  // phase-shift the voltage waveform to align with the current(when a resistive load is used)
  long  phaseShiftedSampleVminusDC_long = lastSampleVminusDC_long
         + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_int)>>8); 

  // determine the instantaneous power content, for use later
  long filtV_div4 = phaseShiftedSampleVminusDC_long>>2;  // reduce to 16-bits (now x64, or 2^6)
  long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
       instP = filtV_div4 * filtI_div4;  // 32-bits (now x4096, or 2^12)
       instP = instP>>12;     // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
       
   // determine polarity, to save time later
  if(sampleVminusDC_long > 0) {
    polarityNow = POSITIVE; }
  else {
    polarityNow = NEGATIVE; }
 
   // Check for the start of a new mains cycle
  if (polarityNow != polarityOfLastSampleV) {
    newHalfCycleFlag = true; }
  else { 
    newHalfCycleFlag = false; }

  if(sampleVminusDC_long > triggerThreshold_long) {
     voltageOKToArmTriggerFlag = true; }
  else {
    voltageOKToArmTriggerFlag = false; }   
   
  // store items that need to be carried over to the next loop
//  lastSampleV = sampleV;  // required for HPF
//  lastSampleI = sampleI;  // required for HPF
//  lastFilteredV_long = filteredV_long;  // required for HPF
  lastSampleVminusDC_long = sampleVminusDC_long;  // required for phaseCal algorithm
//  lastFilteredI_long = filteredI_long;  // required for HPF
  polarityOfLastSampleV = polarityNow;  // for identification of half cycle boundaries
}


void processAllOtherTasks()
{
  if (newHalfCycleFlag == true)
  {
    if (polarityNow == POSITIVE)
    {                           
      // This is the start of a new +ve half cycle (just after the zero-crossing point)
      cycleCount++; 
      triggerNeedsToBeArmed = true;   
      // If required, this is a good place from which to call checkLedStatus()     
   
      // Calculate the real power and energy during the last whole mains cycle.
      //
      // sumP contains the product of filtered V_ADC and I_ADC samples, just as for Mk2 and
      // many other sketches.  Because only integer maths is being used for Mk2a, things have
      // to be scaled differently from this point.
      //
      // sumP contains the sum of many individual calculations of instant power.  In order
      // to obtain the average power during the relevant period, sumP must first be divided
      // by the number of samples that have contributed to its value.
      //
      // The next stage would normally be to apply a calibration factor so that real power
      // can be expressed in Watts.  That's fine for floating point maths, but it's not such
      // a good idea when integer maths is being used.  To keep the numbers large, and also
      // to save time, calibration of power is omitted at this stage.  realPower_long is
      // therefore (1/powerCal) times larger than the actual power in Watts.
      //
      long realPower_long = sumP / samplesDuringThisCycle; // proportional to Watts
   
      // Next, the energy content of this power rating needs to be determined.  Energy is
      // power divided by time, so the next step is normally to divide by the time over which
      // the power was measured.  For the _rev3 version, power is measured every whole
      // mains cycle, so that's 50 times per second.  When integer maths is being used, this
      // stage seems unnecessary.  As all sampling periods are of similar duration (20mS), it
      // is more efficient simply to add all the power samples together, and note that their
      // sum is actually 50 times greater than it would otherwise be.
      //
      // Although the numerical value itself does not change, I think a new name would be
      // helpful so as to avoid any confusion.  The'energy' variable below is 50 * (1/powerCal)
      // times larger than the actual energy in Joules.
      //
      long realEnergy_long = realPower_long;
   
      // Energy contributions are summed in an accumulator which is generally known as the
      // energy bucket.  The purpose of the energy bucket is to mimic the operation of the
      // supply meter.  Most supply meters have a range of 0.001kWh within which energy can
      // pass to and fro without loss or charge to the user.  The energy bucket in the Mk2
      // Power Router was therefore to 0.001kWh, or 3600 Joules.  Moreover, its contents
      // were correctly pre-scaled to be in Joules.
      //
      // As described above, energy contributions for the Mk2a version are scaled somewhat
      // higher that for its floating point predecessor.  The capacity of the energy bucket for
      // Mk2a _rev3 thereore needs to be 3600J * 50 * (1/powerCal).  This is the value that
      // appears in setup().
   
   
      //----------------------------------------------------------------------------------
      // WARNING - Serial statements can interfere with time-critical code, use with care!
      //----------------------------------------------------------------------------------
/*     
      if(((cycleCount % 50) == 5) && polarityNow == POSITIVE) // display once per second
      {
        Serial.print(realPower_long);
        Serial.print(", ");
        Serial.print(energyInBucket_long);
        Serial.print(", ");
        Serial.println(energyInBucket_4led_long); // has no upper or lower limits
        energyInBucket_4led_long = 0; // useful for calibration/test purposes
      }
*/
      if (beyondStartUpPhase)
      { 
        // Providing that the initial settling time has passed,   
        // add this latest contribution to the energy bucket
        energyInBucket_long += realEnergy_long;   
        energyInBucket_4led_long += realEnergy_long;   
         
        // Apply max and min limits to bucket's level.  This is to ensure correct operation
        // when conditions change, i.e. when import changes to export, and vici versa
        //
        if (energyInBucket_long > capacityOfEnergyBucket_long)
        {
          energyInBucket_long = capacityOfEnergyBucket_long;
          digitalWrite(outputPinForLed, 1); // illuminate the on-board LED if bucket overflows
        }
        else
        {
          digitalWrite(outputPinForLed, 0); // clear the on-board LED if bucket is not overflowing
          if (energyInBucket_long < 0)
          {
            energyInBucket_long = 0;
          } 
        }
 
#ifdef TALLYMODE
        tallymode_updateData(realPower_long); // update the relevant tally
#endif
      }
      else
      { 
        // check whether the system had time to settle
        if(cycleCount > (startUpPeriod * CYCLES_PER_SECOND))
        {
          beyondStartUpPhase = true;
//        Serial.println ("go!");
        }
      }   
     
      // clear the per-cycle accumulators for use in this new mains cycle. 
      samplesDuringThisCycle = 0;
      sumP = 0;

    } // end of processing that is specific to the first Vsample in each +ve half cycle   
    else
    {     
      // This is the start of a new -ve half cycle (just after the zero-crossing point)
      //
      // This is a convenient point to update the Low Pass Filter for DC-offset removal ...
      long previousOffset = DCoffset_V_long;
      DCoffset_V_long = previousOffset + (0.01 * cumVdeltasThisCycle_long);
      cumVdeltasThisCycle_long = 0;
     
      // ... and prevent the LPF's output from drifting beyond the likely range of the voltage signal
      if (DCoffset_V_long < DCoffset_V_min) {
        DCoffset_V_long = DCoffset_V_min; }
      else 
      if (DCoffset_V_long > DCoffset_V_max) {
        DCoffset_V_long = DCoffset_V_max; }
           
      // The triac can change state at each -ve going zero crossing.  Update the state of a
      // flag which shows the current state of the triac (for anti-flicker mode logic)
      //     
      if (nextStateOfTriac == ON) {
        triacState = ON; }
      else { 
        triacState = OFF; }   
    } // end of processing that is specific to the first Vsample in each -ve half cycle
  }
 
  if (polarityNow == POSITIVE)
  {
    // for whole-cycle operation, the trigger is only armed during +ve half cycles
    if (triggerNeedsToBeArmed == true)
    {
      // check to see whether the trigger device can now be reliably armed
      if(voltageOKToArmTriggerFlag == true) 
      {
        if (operatingMode == NORMAL)
        {
          // check to see whether the energy threshold has been reached
          if (energyInBucket_long > energyThreshold_long)
          {
            nextStateOfTriac = ON; 
          }
          else
          {
            nextStateOfTriac = OFF;
          }
        }
        else
        {
          // We're in anti-flicker mode, so different logic applies ...
          //
          if (energyInBucket_long < antiFlicker_lowerEnergyThreshold)       
          {
            // when below the lower threshold, always turn the triac off
            nextStateOfTriac = OFF; 
          }
          else
          if (energyInBucket_long > antiFlicker_upperEnergyThreshold) // upper threshold     
          {
            // when above the upper threshold, ensure that the triac is on if permitted
            if (triacState != ON)
            {
              if (cycleCount > cycleCountAtLastActivation + minCycleCountsBetweenActivations)
              {
                nextStateOfTriac = ON;  // the external trigger device is active low
                cycleCountAtLastActivation = cycleCount;
              }
            }
          }
          else
          {
            // the energy level is between the upper and lower thresholds, so
            // leave the triac's state unchanged (anti-flicker measure)
          }         
        } // end of anti-flcker mode logic
                 
        // set the Arduino's output pin accordingly, and clear the flag
        digitalWrite(outputPinForTrigger, nextStateOfTriac);   
        triggerNeedsToBeArmed = false;     
      }
    }
  }
  else
  {
    // When the voltage polarity is negative, no special processing is needed.
  }

  // Rest of processing for ALL Vsamples
  //  (this has to go here because the counters / accumulators may have just been cleared)
  sumP +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
  cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
  samplesDuringThisCycle++;
}
//  ----- end of main Mk2a code -----


#ifdef TALLYMODE
void tallymode_checks()
{
  if (tallymode_firstLoop) {
    tallymode_setup(); } // user-dialogue for recording energy data
  else
  if (cycleCount >= tallymode_maxCycleCount) {
    tallymode_dispatchData(); // send recorded energy data to the Serial monitor 
//    cycleCount = 0;
    tallymode_firstLoop = true;  // get ready for another run
    pause(); } // so that user can access data from the Serial monitor

  else
  if (beyondStartUpPhase) {
      tallymode_noOfSamplePairs++; }
}


void  tallymode_setup()
{
  char inbuf[10];
  int tempInt;
  byte noOfBytes;
  boolean done;

 // <-- start of commented out section to save on RAM space.
 /*
   Serial.println ("WELCOME TO TALLYMODE ");
  Serial.println ("This mode of operation allows the energy content of individual mains cycles");
  Serial.println ("to be analysed.  For nomal operation, the #define TALLYMODE statement");
  Serial.println ("should be commented out. ");
*/
   // <-- end of commented out section to save on RAM space.

  Serial.println (); 
  Serial.println ("Tallymode setup:"); 
  Serial.print ("Time to run (secs)? ");
  done = false;
  while (!done) {
    noOfBytes = Serial.available();
    if (noOfBytes > 0) { done = true; } else { delay(100); }}
  for (tempInt = 0; tempInt < 10; tempInt++) { inbuf[tempInt] = 0; }
  Serial.readBytes(inbuf, noOfBytes);
  tempInt = atoi(inbuf);  Serial.println (tempInt);
  tallymode_maxCycleCount = tempInt * CYCLES_PER_SECOND;
  tallymode_durationOfRecording = tempInt;
 
  Serial.print ("Min value (Watts)? ");
  done = false;
  while (!done) {
    noOfBytes = Serial.available();
    if (noOfBytes > 0) { done = true; } else { delay(100); }}
  for (tempInt = 0; tempInt < 10; tempInt++) { inbuf[tempInt] = 0; }
  Serial.readBytes(inbuf, noOfBytes);
  tempInt = atoi(inbuf);  Serial.println (tempInt);
  tallymode_minVal = tempInt;
//  Serial.print(" tallymode_minVal = "); Serial.println(tallymode_minVal);   
//  tallymode_minVal_long = tallymode_minVal * (1/powerCal); // used for power, not energy,
                                                           // hence there's no x100 term.
 
  Serial.print ("Max value (Watts)? ");
  done = false;
  while (!done) {
    noOfBytes = Serial.available();
    if (noOfBytes > 0) { done = true; } else { delay(100); }}
  for (tempInt = 0; tempInt < 10; tempInt++) { inbuf[tempInt] = 0; }
  Serial.readBytes(inbuf, noOfBytes);
  tempInt = atoi(inbuf);  Serial.println (tempInt);
  tallymode_maxVal = tempInt;
//  Serial.print(" tallymode_maxVal = "); Serial.println(tallymode_maxVal);   

  if (tallymode_maxVal <= tallymode_minVal) {
    Serial.print(7); // beep
    Serial.println ("ERROR!"); }
 
  tallymode_stepVal = (float)(tallymode_maxVal - tallymode_minVal) / NUMBER_OF_TALLIES;
//  Serial.print(" tallymode_stepVal = "); Serial.println(tallymode_stepVal);   
/*
  tallymode_stepVal_long = tallymode_stepVal * (1/powerCal); // used for power, not energy,
                                                             // hence there's no x50 term.
  tallymode_halfStep = (tallymode_stepVal_long / 2); // to avoid integer-rounding
*/

  for (tempInt = 0; tempInt < NUMBER_OF_TALLIES + 2; tempInt++) {
    tally[tempInt] = 0; }
   
  energyInBucket_long = (capacityOfEnergyBucket_long / 2); // for rapid startup of power distribution
  cycleCount = 0;
  tallymode_noOfValuesTallied = 0;
  tallymode_noOfSamplePairs = 0;
  beyondStartUpPhase = false; // to allow LPF to re-settle for next run
  tallymode_firstLoop = false;

  Serial.print(" Recording will start in ");
  Serial.print(startUpPeriod);   
  Serial.println(" seconds ... ");
 
 // startTime = millis();
};


void  tallymode_updateData(long power_long)
{
  float powerInWatts = power_long * powerCal;
  int index;
 
  if (powerInWatts < tallymode_minVal)
  {
    index = 0; // tally[0] is for underflow
  }
  else
  if (powerInWatts > tallymode_maxVal)
  {
    index = NUMBER_OF_TALLIES + 1; // tally[N+1] is for overflow
  }
  else
  {
    index = (powerInWatts - tallymode_minVal) / tallymode_stepVal;
    index += 1; // because the linear tallies run from 1 to N, not from 0
  }
 
  tally[index]++; // increment the relevant tally   
  tallymode_noOfValuesTallied++;
}


void  tallymode_dispatchData()
{
 
//   <<- start of commented out section, to save on RAM space!
/*   
  Serial.println ();
  Serial.println ("Format of results: ");
  Serial.print ("Sorted data runs from tally[1] (min) to tally[");
  Serial.print (NUMBER_OF_TALLIES);
  Serial.println ("] (max).");
  Serial.print ("tally[0] is below range; tally[ ");
  Serial.print (NUMBER_OF_TALLIES + 1);
  Serial.println ("] is above range.");
  Serial.println("Tally results are displayed with max power first. For each one:");
  Serial.println("  mid-range power of tally (Watts), number of times recorded.");
  Serial.println();
*/   
//   <<- end of commented out section, to save on RAM space!
 
  Serial.println("Results for this run:");
  Serial.print (tallymode_minVal);
  Serial.println (", <- min value of tally range (W)");
  Serial.print (tallymode_maxVal);
  Serial.println (", <- max value of tally range (W)");
  Serial.print (tallymode_stepVal);
  Serial.println (", <- step value between tallies (W)");
  Serial.print (NUMBER_OF_TALLIES);
  Serial.println (", <- No. of tallies");
  Serial.print (tallymode_durationOfRecording);
  Serial.println (", <- duration (secs)");
  Serial.print (tallymode_noOfValuesTallied);
  Serial.println (", <- no of values tallied");
  Serial.print ( tallymode_noOfSamplePairs /
    ((float)tallymode_durationOfRecording * CYCLES_PER_SECOND), 1);
  Serial.println (", <- loops per mains cycle (av)");
  Serial.println("***");
 
  float powerVal;
  for (int index = NUMBER_OF_TALLIES + 1; index >= 0; index--)
  {
/* 
    // display the index value for this tally
    Serial.print (index);
    Serial.print (", ");
*/   
    // display the power value for this tally
    if (index == NUMBER_OF_TALLIES + 1)
    {
      Serial.print (">");
      Serial.print (tallymode_maxVal);
    }
    else
    if (index == 0)
    {
      Serial.print ("<");
      Serial.print (tallymode_minVal);
    }
    else 
    {
      if (index == NUMBER_OF_TALLIES)
      {
        powerVal = tallymode_maxVal - (tallymode_stepVal / 2);
      }
      else
      {
        powerVal -= tallymode_stepVal;
      }
     
      if ((int)powerVal == powerVal)
      {
        Serial.print (powerVal, 0); // to suppress the decimal part for integers
      }
      else
      {
        Serial.print (powerVal);  // non-integers are displayed to 2 dec places
      }
    } 
    Serial.print ("W");

    // display the number of hits for this tally
    Serial.print (", ");
    Serial.println (tally[index]); 
  }
};


void pause()
{
  byte done = false;
  byte dummyByte;
   
  while (done != true)
  {
    if (Serial.available() > 0) {
      dummyByte = Serial.read(); // to 'consume' the incoming byte
      if (dummyByte == 'g') done++; }
  }   
}
#endif


// helper function, to process LED events, can be conveniently called once per mains cycle
void checkLedStatus()
{
  ledState = digitalRead (ledDetectorPin);

  if (ledState != prevLedState)
  {
    // led has changed state
    if (ledState == ON)
    {
      // led has just gone on
      ledOnAt = millis();
      ledRecentlyOnFlag = true;
    }
    else
    {
      // led has just gone off   
      if (ledRecentlyOnFlag == true)
      {
        ledRecentlyOnFlag = false;       
        Serial.print ("** LED PULSE ** "); // this is a chargeable event 
      }
      else
      {
        Serial.print ("** LED OFF ** "); // 'no longer exporting' is also a chargeable event
      }
      Serial.println(millis()/1000);
    }   
  }
  else
  {
    // the LED state has not changed
    if (ledState == ON)
    {
      if (ledRecentlyOnFlag == true)
      {
        // check to see if the known duration of a pulse has been exceeded   
        unsigned long timeNow = millis();     
        if ((timeNow - ledOnAt) > 50)
        {
          Serial.print ("** LED ON **");  // 'exporting' is a non-chargeable state     
          Serial.print (",  energy in bucket = ");
          Serial.println((long)(energyInBucket_4led_long));
          ledRecentlyOnFlag = false;   
        }     
      }
    }
  }
  prevLedState = ledState;   
}

mktutsi

  • Tulokas
  • *
  • Viestejä: 31
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #48 : 02.11.18 - klo:15:26 »
Eli mk2pv ajaa "NORMAL" modessa kokonaisia syklejä päälle/pois? Ja silti sähkömittarilla tuotanto ja kulutus tasapainottuu? Höh, mun mittari kiukutteli niin kauan kunnes vaihdoin vaihekulmasäätöön.

Oletko kokeillut "ANTI FLICKER", toimiiko sähkömittari silläkin? Mikä mittari sulla on? Voiko noissa mittareissa olla noin oleellisia eroja.

jolla

  • Aktiivijäsen
  • ***
  • Viestejä: 575
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #49 : 02.11.18 - klo:15:31 »
toimii molemmilla, minulla ei ole mittari kiukutellut yhtään, invertterit ajaa alas jos yrittää yli 2kw säätää

tässä kuva hyvin 'haastavista' olosuhteista kun kirkkaalla säällä pilviä tulee ja menee, näkyy skaalautuvuus

hyvin näkyy kun tiski/pyykkikone olleet 'vika' aikaan päällä ja vimonen kulutuspiikki on sauna
« Viimeksi muokattu: 02.11.18 - klo:15:49 kirjoittanut jolla »

Ilmaisenergia.info

Vs: lämminvesivaraajan tehonsäätö
« Vastaus #49 : 02.11.18 - klo:15:31 »

tengu

  • Aktiivijäsen
  • ***
  • Viestejä: 563
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #50 : 02.11.18 - klo:15:54 »
Täällä mk2 ja 3 ulostuloa käytössä. 1 1+kw   2 2kw 3 2kw.  Aina kun yksi pykälä on 100% ja silti yrittää mennä ulos niin edellinen pysyy täysin auki ja aletaan seuraavaa pulssittamaan. Toimii samoin kun jollalla mutta ihan vain yhdellä arduinopohjaisella härvelillä. Hyvin pitää Carunan mittarin nollilla päivisin vaikka ajan täysiä puolijaksoja. Kaikki pykälät taitaa olla vielä eri vaiheissa.

Miten olen itse antanut itseni ymmärtää tuon sähkömittarin noin toimintaperiaatteen:

Mittari 1000 pulssia kWh.
1 pulssi on 1 Ws.
Mittari laskee tulevan ja lähtevän sähkön erikseen. Niin kauan kun sähköä kuluu lisätään pykäliä "käyttörekisteriin"  ja kun käyttörekisteri tulvii (yli 1 Ws)  niin se nollataan ja lisätään kulutusrekisteriin (tai tuotto) pulssi. Käyttörekisteri lisääntyy kun sähköä kuluu ja vähenee kun tuotetaan. Niin kauan kun ei mennä rajojen yli, että rekisteri nollaantuu, ei mitään lasketa. Pulssi mikä menee kulutusrekisteriin on siellä ja pysyy.

Teknisesti miten se mittaus sitten tuolla käyttörekisterin sisällä käytönnössä toimii niin se onkin varmaan valmistajan teollisuussalaisuus.

3x25 tyhjiöputkea. 2,1kw PV verkossa Zeverlution 2000S. 1,2Kw PV panelia verkossa Sunny Boy 1200 SB. 240w verkossa AEConversion. 500w verkossa Envertech. 2000L varaaja+60L tulistus kv:lle. MK2PVRouter omakäytön maksimointiin.

pere

  • Jäsen
  • **
  • Viestejä: 56
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #51 : 02.11.18 - klo:16:58 »
Mielenkiintoisia vastauksia!
Enpä taida omaa ssr/pwm systeemiä vielä purkaa vaan odottelen että olisi jotain säädettävää (ja testattavaa)
Tällä hetkellä kaikki mikä tulee menee kulutukseen eikä edes riitä.
Säätövaranahan minulla on vain lvv:n 3kpl 1kw vastusta ja jokainen eri vaiheessa.
Eilen tuli paneeleista 1.03 kwh ja tänään hiukan vähemmän.

kotte

  • Aktiivijäsen
  • ***
  • Viestejä: 1044
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #52 : 02.11.18 - klo:17:21 »
Moniportainen nollapistekytkin/SSR tietenkin toimii ihan hyvin, jos on riittävästi tehoportaita ja valitsee aina sopivan vastuskombinaation. Voi esimerkiksi hommata 1kW, 500W, 250W ja 125W vastukset. Noista saa kytkemällä 0W, 125W, 250W, 375W, 500W, 625W, 750W, 875W, 1kW, ... , 1875W tehovastukset ja noillahan saa joko verkosta imetyn tai sinne turhaan uhratun tehon +-78W tarkkuusrajoissa nollaan lähes 2kW:n tehoon asti. Tuo lienee moniin käytännön tuloksiin riittävä tarkkuustaso, teknisesti ongelmaton ja melko helppo toteuttaa.

pere

  • Jäsen
  • **
  • Viestejä: 56
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #53 : 02.11.18 - klo:18:08 »
Moniportainen nollapistekytkin/SSR tietenkin toimii ihan hyvin, jos on riittävästi tehoportaita ja valitsee aina sopivan vastuskombinaation. Voi esimerkiksi hommata 1kW, 500W, 250W ja 125W vastukset. Noista saa kytkemällä 0W, 125W, 250W, 375W, 500W, 625W, 750W, 875W, 1kW, ... , 1875W tehovastukset ja noillahan saa joko verkosta imetyn tai sinne turhaan uhratun tehon +-78W tarkkuusrajoissa nollaan lähes 2kW:n tehoon asti. Tuo lienee moniin käytännön tuloksiin riittävä tarkkuustaso, teknisesti ongelmaton ja melko helppo toteuttaa.
Kyllä kyllä mutta 3~ tapauksessa tuo vastusten määrä tulee kertoa kolmella, koska tasaus tulee suoritta jokaiselle vaiheelle.
Ja kun sitä yritetää käyttää vedenlämmitykseen niin eipä se ihan helppoa olekkaan, mistä voi löytää varaajan jossa olisi noin paljon vastuksia?
Luulisin että ainoa realistinen vaihtoehto on toimiva tehonsäätö. Miten se pitäisi tehdä,siitä tässä on kysymys, ainakin minulla.

kotte

  • Aktiivijäsen
  • ***
  • Viestejä: 1044
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #54 : 02.11.18 - klo:18:58 »
Jollalle kysymys.
Aikaisemmista viesteistäsi ja "korsteeni" kuvista päättelin että sinulla on säädössä SSR.
Kun nyt täällä ollaan sitämieltä että se ei oikein toimi niin miten sinulla toimii?
Vai olenko käsittänyt väärin?
Jotenkin tuo tehon ohjaaminen ssr/pwm tuntuu "tyylikkäämmältä"!

kyllä se toimii SSR/pwm, ongelmia oli saada tarpeeksi stabilia ja luotettavaa ohjausdataa ja yli 2kW alkoivat tuottaa ongelmia kuten myös tällä mk2pv'llä mikä nyt on käytössä
ongelma ratkesi toisella arduinolla mikä ei tee muuta kun kytkee SSR kanssa lisäkuormaa kiinetästi tarvittaessa, uskoisin että ssr/pwm toimisi myös tällä logiikalla

nyt on käytössä linkin http://www.elisanet.fi/korsteeni/mk2pv.shtml toiseksi alimman kuvan mukainen kytkentä millä toimii moitteetta 1,6kW vastuksen kanssa ja lisäkuormilla (se toinen arduino) 7kW saakka on toimiminut saumattomasti

pwm'ällä eikä pulssilla yli 2kW kuomilla ei saa toimimaan näillä linjoilla tässä taloudessa

ihan rautalankana
- kun tuotto ( panelien tuotto - kulutus) on 0-1,6 kW mk2pv hoitaa säädön 1,6kw vastuksella
- yli 1,6kW toinen arduino kytkee 1,6 kw vastuksen kiinteästi
- yli 3,2 kw kytkee 2,5kw ja 1,6kW irti
- jne

Koodia: [Valitse]
// EmonLibrary examples openenergymonitor.org, Licence GNU GPL V3

#include "EmonLib.h"             // Include Emon Library
EnergyMonitor emon1;             // Create an instance
int upPin7 = 7;
int upPin8 = 8;
int upPin9 = 9;

void setup()
{
  pinMode(upPin7,OUTPUT);
  pinMode(upPin8,OUTPUT);
  pinMode(upPin9,OUTPUT); 
  Serial.begin(9600);
 
  emon1.voltage(2, 150, 1.7);  // Voltage: input pin, calibration, phase_shift
  emon1.current(1, 12.7);       // Current: input pin, calibration.
}

void loop()
{
  emon1.calcVI(20,2000);         // Calculate all. No.of half wavelengths (crossings), time-out
  emon1.serialprint();           // Print out all variables (realpower, apparent power, Vrms, Irms, power factor)
 
  float realPower       = emon1.realPower;        //extract Real Power into variable
  float apparentPower   = emon1.apparentPower;    //extract Apparent Power into variable
  float powerFActor     = emon1.powerFactor;      //extract Power Factor into Variable
  float supplyVoltage   = emon1.Vrms;             //extract Vrms into Variable
  float Irms            = emon1.Irms;             //extract Irms into Variable
 
    if ((realPower >= 1600) && (realPower < 3200)) //170W tullut yli 28 p�iv�n�
    {
    digitalWrite(upPin7, HIGH);
    digitalWrite(upPin8, LOW);
    digitalWrite(upPin9, LOW);
    }
    else if ((realPower >= 3200) && (realPower < 4100))
    {
    digitalWrite(upPin8, HIGH);
    digitalWrite(upPin7, LOW);
    digitalWrite(upPin9, LOW);
    }
    else if ((realPower >= 4100) && (realPower < 5400))
    {
    digitalWrite(upPin8, HIGH);
    digitalWrite(upPin7, HIGH);
    digitalWrite(upPin9, LOW);
    }
    else if ((realPower >= 5400) && (realPower < 8200))
    {
    digitalWrite(upPin8, HIGH);
    digitalWrite(upPin9, HIGH);
    digitalWrite(upPin7, LOW);
    }
    else
    {
    digitalWrite(upPin7, LOW);
    digitalWrite(upPin8, LOW);
    digitalWrite(upPin9, LOW);
    } 
}


käytössä oleva mk2pv koodi

Koodia: [Valitse]
// Mk2a is a revised version of my stand-alone sketch for diverting suplus
// PV power to a dump load using a triac.  The original Mk2 version, which includes
// more explanation and 'debug' code to support off-line working, may be found at
// http://openenergymonitor.org/emon/node/841
//
// General aspects of Mk2a:
// The Mk2a code is suitable for hardware that has multiple voltage references, such
// as emonTx.  Mk2a uses integer maths for improved speed.  It also uses low-level
// instructions for ADC operations which 'frees up' time that was previously unavailable
// for general processing activities.  Further information about this enhancement may be
// found immediately before loop().
//
// Specific releases of Mk2a:
// The initial release of Mk2a had twin high-pass filters, as used in the standard OEM
// V&I sketch.  Due to a couple of minor bugs, it was soon replaced by the _rev2 version.
//
// The _rev3 version marks a further change to the way that the raw sample streams are
// processed.  This has come about because, for the purpose of calculating "real power",
// there is no need for DC offset to be pre-removed from the raw current samples. The
// standard calculation for real power does this automatically when applied over each whole
// cycle of the mains.  Half-cycle processing of power has been discontinued.
//   A single LPF, which is updated just once per mains cycle, has been introduced for
// the purpose of determining the DC offset of the voltage waveform.  This value is then
// subtracted from each raw voltage sample.  A nominal value of 512 is subtracted from
// each current sample, this being to prevent the system becoming over-sensitive to any
// imbalance conditions or random noise; the actual value is unimportant.
//
// The ANTI_FLICKER option was also introduced in _rev3.  This mode uses two thresholds
// for the energy bucket rather than just one.  When surplus power is available,
// the power-diversion logic operates the triac between these two limits on a hysteresis
// basis.  To ensure that the anti-flicker requirement is always met, a minimum amount of
// time has to elapse between consecutive activations of the triac.  Under certain
// conditions, this may cause a small amount of surplus power to be lost when the bucket
// overflows.  The on-board LED (pin 13) shows whenever then this occurs. Accurate
// calibration of the system is essential when operating in this mode.
// 
// TALLYMODE is a #define option which allows energy data to be collected for
// subsequent display.  In _rev2, energy values in positive and negative half-cycles was
// recorded separately.  From _rev3 onwards, energy is recorded for whole cycles only.
//   While running in TALLYMODE, the code is fully functional.  It pauses after a
// user-selectable period so that stored data can be retrieved.  To avoid overflow, the
// maximum recommended duration is 10 minutes.  To start again, press the 'g' key, then
// carriage-return.
//   TALLYMODE provides a very easy way to calibrate the system.  Clip the CT around
// one core of a cable that supplies a known load, and do a short run with appropriate
// Min and Max values.  If the peak distribution (in Watts) is not where you expect it
// to be, then just change the value of powerCal until it is. 
//   Full details about calibration are provided just before setup().
//
// SPEEDCHECK is another #define option.  When #included, the results of a built-in
// checker facility are displayed.  This facility keeps track of the number of times
// that general processing fails to complete within the duration of the associated
// ADC conversion.  SPEEDCHECK may be left on permanently, if desired.
//   If additional workload is ever to be added, SPEEDCHECK will provide a sensitive
// indication of any undesirable effect on the underlying code.  Any value other than
// zero will reduce the accuracy of power/energy calculations.
//
// Mk2a_rev3a.  This corrects a minor but long-standing error in the data produced by
// Tallymode.  Tallymode data is now displayed in Watts, rather than in Tally numbers.
//
//                  Robin Emley (calypso_rae on Open Energy Monitor Forum)
//                  June 2013


/*
Circuit for monitoring pulses from a supply meter using checkLedStatus():
 
                  ----------------> +5V
                  |
                  /
                  \ 8K2
                  /
                  |
             ---------------------> dig 2
             |       |
        -->  /       |
        -->  \       _
        LDR  /       -  0.01uF
             |       |
             ----------------------> GND
*/

// There are two #define options which may be activiated independently
//
//#define TALLYMODE // for recording energy data.  Comment out for normal operation
//#define SPEEDCHECK // to display the results of a built-in checker every few seconds

enum operatingModes {NORMAL, ANTI_FLICKER};
enum operatingModes operatingMode = NORMAL; // <-- select the desired mode here

#define CYCLES_PER_SECOND 50
#define JOULES_PER_WATT_HOUR 3600 // 0.001 kWh = 3600 Joules
enum polarities {NEGATIVE, POSITIVE};
enum triacStates {ON, OFF}; // the external trigger device is active low

// general global variables
const byte outputPinForLed = 13;  // digital
const byte outputPinForTrigger = 9; // digital
const byte ledDetectorPin = 2;  // digital
const byte ledRepeaterPin = 10;  // digital
const byte voltageSensorPin = 2;  // analogue
const byte currentSensorPin = 1;   // analogue
const byte startUpPeriod = 5; // in seconds, to allow HP filters to settle
const int DCoffset_I = 512; // for calculating "real power", a nominal value is fine

long cycleCount = 0;
long cycleCountAtLastActivation = 0;
int samplesDuringThisCycle;
enum triacStates nextStateOfTriac;
enum triacStates triacState;
boolean triggerNeedsToBeArmed;
boolean beyondStartUpPhase = false;
float safetyMargin_WattsPerHalfCycle;
long triggerThreshold_long;
long energyInBucket_long = 0; // for integer maths
int phaseCal_int; // for integer maths
long capacityOfEnergyBucket_long;  // for integer maths
long energyThreshold_long;  // for integer maths
long antiFlicker_lowerEnergyThreshold;
long antiFlicker_upperEnergyThreshold;
long sumP;// for integer maths
long DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
long DCoffset_V_min;
long DCoffset_V_max;

float minTimeBetweenActivations = 3; // <-- anti-flicker requirement (seconds)
int minCycleCountsBetweenActivations;

// items to allow general processing tasks to be efficiently partitioned.
boolean newHalfCycleFlag;
boolean voltageOKToArmTriggerFlag;
enum polarities polarityNow;
long instP;
long sampleVminusDC_long;

// values that need to be stored from one loop to the next
long lastSampleVminusDC_long;  //    for phaseCal algorithm
enum polarities polarityOfLastSampleV; // for zero-crossing detection
long cumVdeltasThisCycle_long;   // <<--- for LPF
int sampleV_forNextLoop; // for deferred processing
int sampleI_forNextLoop; // for deferred processing

// items for LED monitoring
byte ledState, prevLedState;
boolean ledRecentlyOnFlag = false;
unsigned long ledOnAt;
long energyInBucket_4led_long = 0;

// for the in-built mechanism to check whether the ADC conversion time is ever exceeded
boolean speedFlag;
long FlagNotClearedCounter = 0;
long loopCountForSpeedChecker = 0;
long maxLoopCountForSpeedChecker = 20000; // how often to display results (~ 4500 loops/sec)

#ifdef TALLYMODE
#define NUMBER_OF_TALLIES 100
unsigned int tally[NUMBER_OF_TALLIES + 2]; // For recording the energy content in whole mains cycles.
unsigned int tallymode_maxCycleCount;  // the cycleCount value when recording should cease
int tallymode_maxVal; // the maximum power to be recorded (Watts)
int tallymode_minVal; // the minimum power to be recorded (Watts)
//long tallymode_minVal_long; // x256 version for integer maths
float tallymode_stepVal; // the power increment between consecutive tallies (Watts)
//long tallymode_stepVal_long; // x256 version for integer maths
boolean tallymode_firstLoop = true;
unsigned int tallymode_noOfValuesTallied; // overflows after 10.9 minutes
unsigned long tallymode_noOfSamplePairs; // to show the average samples-per-mains-cycle rate
int tallymode_durationOfRecording; // in seconds
#endif

// Calibration values
//-------------------
// Three calibration values are required: powerCal, voltageCal and phaseCal.
// With most hardware, the default values are likely to work fine without
// need for change.  A full explanation of each of these values now follows:
//   
// powerCal is a floating point variable which is used for converting the
// product of voltage and current samples into Watts.
//
// The correct value of powerCal is entirely dependent on the hardware that is
// in use.  For best resolution, the hardware should be configured so that the
// voltage and current waveforms each span most of the ADC's usable range.  For
// many systems, the maximum power that will need to be measured is around 3kW.
//
// My sketch "MinAndMaxValues.ino" provides a good starting point ideal for
// system setup.  First arrange for the CT to be clipped around either core of a 
// cable which supplies a suitable load; then run the tool.  The resulting values
// should sit nicely within the range 0-1023.  To allow some room for safety,
// a margin of around 100 levels should be left at either end.  This gives a
// output range of around 800 ADC levels, which is 80% of its usable range.
//
// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the
// voltage and current waveforms.  This provides an easy way for the user to be
// confident that their system has been set up correctly for the power levels
// that are to be measured.
//
// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
// The purpose of each input sensor is to convert the measured parameter into a
// low-voltage signal which fits nicely within the ADC's input range.
//
// In the case of 240V mains voltage, the numerical value of the input signal
// in Volts is likely to be fairly similar to the output signal in ADC levels. 
// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal
// output range.  Stated more formally, the conversion rate of the overall system
// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS).
//
// In the case of AC current, however, the situation is very different.  At
// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which
// has a peak-to-peak range of 35A.  This is smaller than the output signal by
// around a factor of twenty.  The conversion rate of the overall system for
// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
//
// When measuring power, which is what this code does, the individual
// conversion rates for voltage and current are not of importance.  It is
// only the conversion rate for POWER which is important.  This is the
// product of the individual conversion rates for voltage and current.  It
// therefore has the units of ADC-steps squared per Watt.  Most systems will
// have a power conversion rate of around 20 (ADC-steps squared per Watt), so
// is a good default value to start with.
//
// powerCal is the RECIPR0CAL of the power conversion rate.  A good value
// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared)
//
const float powerCal = 0.067;  // <---- powerCal value
 
// As mentioned above, the conversion rate for AC voltage has units of 
// ADC-steps per Volt.  Athough not required for measuring power, this
// conversion rate does need to be known in order to determine when the
// voltage level is suitable for arming the external trigger device.
//
// To determine the voltage conversion rate, note the Min and Max values that
// are seen when measuring 240Vac via the voltage sensor.  Subtract one from
// the other to find the range.  Then put that value into the voltageCal formula
// below.  voltageCal has units of ADC-steps per Volt.
//
// 679 is the peak-to-peak range of a 240V RMS signal.  The (float) typecast
// is to prevent integer rounding
//
const float voltageCal = 656 / (float)679; // <-- the first number is the output range of
                               //     your ADC when 240V AC is being measured
                       
// phaseCal is used to alter the phase of the voltage waveform relative to the
// current waveform.  The algorithm interpolates between the most recent pair
// of voltage samples according to the value of phaseCal.
//
//    With phaseCal = 1, the most recent sample is used. 
//    With phaseCal = 0, the previous sample is used
//    With phaseCal = 0.5, the mid-point (average) value in used
//
// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation
// and are not recommended.  By altering the order in which V and I samples are
// taken, and for how many loops they are stored, it should always be possible to
// arrange for the optimal value of phaseCal to lie within the range 0 to 1.  When
// measuring a resistive load, the voltage and current waveforms should be perfectly
// aligned.  In this situation, the Power Factor will be 1.
//
// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct
// value of phaseCal for any hardware configuration. 
//
const float  phaseCal = 1.0;


void setup()

  Serial.begin(9600);
  pinMode(outputPinForTrigger, OUTPUT); 
  pinMode(outputPinForLed, OUTPUT); 
  Serial.println();
  Serial.println("starting new run ...");
  Serial.println(); 
  Serial.print("DCoffset_V_long = "); 
  Serial.println(DCoffset_V_long); 
 
   
  // When using integer maths, calibration values that have supplied in floating point
  // form need to be rescaled. 
  //
  phaseCal_int = phaseCal * 256; // for integer maths
 
  // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
  // voltage conversion rate that is in use.  This avoids the need to re-scale every
  // energy contribution, thus saving processing time.  To avoid integer rounding,
  // energy is also recorded at x50 scale. 
  //   This process is described in more detail in the function, processAllOtherTasks(),
  // just before the bucket is updated at the start of each new whole-cycle of the mains.
  // 
  capacityOfEnergyBucket_long = (JOULES_PER_WATT_HOUR * 50L) * (1/powerCal);
 
  Serial.print("powerCal = ");
  Serial.print(powerCal,4);
  Serial.println(" Watts per ADCstep^2");
  Serial.print("energy bucket = 3600 * 50 * (1/");
  Serial.print(powerCal,4);
  Serial.print(") = ");
  Serial.print(capacityOfEnergyBucket_long);
  Serial.println(" energy measurement units");
  Serial.println(); 
                                               
  energyThreshold_long = capacityOfEnergyBucket_long * 0.5; // for normal operation
  antiFlicker_lowerEnergyThreshold = capacityOfEnergyBucket_long * 0.1; // for anti-flicker mode
  antiFlicker_upperEnergyThreshold = capacityOfEnergyBucket_long * 0.9; // for anti-flicker mode 
  minCycleCountsBetweenActivations =
       minTimeBetweenActivations * 50; // cycleCount increments every 20mS

  if (operatingMode == NORMAL) {
    energyInBucket_long = 0.8 * energyThreshold_long; } // for faster start-up 
 
  triggerThreshold_long = 50 * 256L / voltageCal;
    // +50V is a suitable point in the rising waveform to arm the zero-crossing trigger.
    // The 256 is because filteredV is scaled at x256 when integer maths is used.
    // The reciprocal of voltageCal converts ADClevels into Volts, as described above.
   
  DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin
  DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin
}

// --------------------------------------------------------------------------------------
// Each time around loop(), a new pair of V & I samples is taken.  Rather than using the
// normal analogRead() statement, the ADC is instructed using low-level instructions. 
// By this means, the ADC's conversion time can be used to process data that was collected
// during the >>PREVIOUS<< iteration of loop(). 
//
// Thanks to the use of integer maths, general processing has been greatly speeded up.
// General processing has also been split into two sections, each of which can fit nicely
// into one of the periods while ADC conversions are taking place. The ADC pre-processor
// is now spending all of its time doing back-to-back V and I conversions, and the main
// Arduino is comfortably able to do all of its processing activities during these
// 'hidden' ADC conversion periods.  The amount of delay that can be attributed to
// general processing activities is now ...  NIL  :-)
//
// The first ADC conversion is for current, that's because the phase of this waveform is
// generally slightly advanced relative to the voltage.  While this first ADC conversion
// is taking place, all of the standard per-loop processing activities are done.
//
// The second ADC conversion is for voltage, that's because this waveform is generally
// slightly retarded relative to the current.  While this second ADC conversion is taking
// place, all of the other general processing activities are done.  This includes the
// updating of the energy bucket, which now occurs twice per mains cycle, and also the
// arming of the zero-crossing trigger.
//
// While restructuring the general processing code, some additional flags have been devised
// so that the overall workload can be partitioned more effectively.  Some variables that
// were previously defined as 'locals' within loop() have now become globals.
//
void loop()             
{
  speedFlag = true;
  ADMUX = 0x40 + currentSensorPin;
  ADCSRA |= (1<<ADSC); // instruct ADC to read the sensor for CURRENT
  {
    processPerLoopTasks(); // all general per-loop processing
    delayMicroseconds(10); // for speed checks only
  }
  while(bit_is_set(ADCSRA, ADSC)) {speedFlag = false;};  // wait for ADC to complete
  sampleI_forNextLoop = ADC;
  FlagNotClearedCounter+= speedFlag; // if the ADC conversion time has been exceeded, it's noted
 
  speedFlag = true;
  ADMUX = 0x40 + voltageSensorPin;
  ADCSRA |= (1<<ADSC); // instruct ADC to read the sensor for VOLTAGE
  {
    processAllOtherTasks(); // all other processing activities
    delayMicroseconds(10); // for speed checks only
  }
  while(bit_is_set(ADCSRA, ADSC)) {speedFlag = false;};  // wait for ADC to complete
  sampleV_forNextLoop = ADC;
  FlagNotClearedCounter+= speedFlag; // if the ADC conversion time has been exceeded, it's noted
 
#ifdef SPEEDCHECK  // this is only processed if needed
  loopCountForSpeedChecker++;
  if (loopCountForSpeedChecker == maxLoopCountForSpeedChecker)
  {
    Serial.println(FlagNotClearedCounter);
    loopCountForSpeedChecker = 0;
    FlagNotClearedCounter = 0;
  }
#endif
} // end of loop()


void processPerLoopTasks()
{
  // all tasks that are required for each loop have been grouped into this routine
#ifdef TALLYMODE
  tallymode_checks();
#endif

  int sampleI = sampleI_forNextLoop; // extract samples taken on previous loop.  This is for clarity
  int sampleV = sampleV_forNextLoop; // only; the stored values won't change during this routine.
 
  // remove DC offset from the raw voltage sample by subtracting the accurate value as determined by a LP filter.
  sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long;

  // remove most of the DC offset from the current sample (the precise value does not matter)
  long sampleIminusDC_long = ((long)(sampleI-DCoffset_I))<<8;
 
  // phase-shift the voltage waveform to align with the current(when a resistive load is used)
  long  phaseShiftedSampleVminusDC_long = lastSampleVminusDC_long
         + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_int)>>8); 

  // determine the instantaneous power content, for use later
  long filtV_div4 = phaseShiftedSampleVminusDC_long>>2;  // reduce to 16-bits (now x64, or 2^6)
  long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
       instP = filtV_div4 * filtI_div4;  // 32-bits (now x4096, or 2^12)
       instP = instP>>12;     // scaling is now x1, as for Mk2 (V_ADC x I_ADC)
       
   // determine polarity, to save time later
  if(sampleVminusDC_long > 0) {
    polarityNow = POSITIVE; }
  else {
    polarityNow = NEGATIVE; }
 
   // Check for the start of a new mains cycle
  if (polarityNow != polarityOfLastSampleV) {
    newHalfCycleFlag = true; }
  else { 
    newHalfCycleFlag = false; }

  if(sampleVminusDC_long > triggerThreshold_long) {
     voltageOKToArmTriggerFlag = true; }
  else {
    voltageOKToArmTriggerFlag = false; }   
   
  // store items that need to be carried over to the next loop
//  lastSampleV = sampleV;  // required for HPF
//  lastSampleI = sampleI;  // required for HPF
//  lastFilteredV_long = filteredV_long;  // required for HPF
  lastSampleVminusDC_long = sampleVminusDC_long;  // required for phaseCal algorithm
//  lastFilteredI_long = filteredI_long;  // required for HPF
  polarityOfLastSampleV = polarityNow;  // for identification of half cycle boundaries
}


void processAllOtherTasks()
{
  if (newHalfCycleFlag == true)
  {
    if (polarityNow == POSITIVE)
    {                           
      // This is the start of a new +ve half cycle (just after the zero-crossing point)
      cycleCount++; 
      triggerNeedsToBeArmed = true;   
      // If required, this is a good place from which to call checkLedStatus()     
   
      // Calculate the real power and energy during the last whole mains cycle.
      //
      // sumP contains the product of filtered V_ADC and I_ADC samples, just as for Mk2 and
      // many other sketches.  Because only integer maths is being used for Mk2a, things have
      // to be scaled differently from this point.
      //
      // sumP contains the sum of many individual calculations of instant power.  In order
      // to obtain the average power during the relevant period, sumP must first be divided
      // by the number of samples that have contributed to its value.
      //
      // The next stage would normally be to apply a calibration factor so that real power
      // can be expressed in Watts.  That's fine for floating point maths, but it's not such
      // a good idea when integer maths is being used.  To keep the numbers large, and also
      // to save time, calibration of power is omitted at this stage.  realPower_long is
      // therefore (1/powerCal) times larger than the actual power in Watts.
      //
      long realPower_long = sumP / samplesDuringThisCycle; // proportional to Watts
   
      // Next, the energy content of this power rating needs to be determined.  Energy is
      // power divided by time, so the next step is normally to divide by the time over which
      // the power was measured.  For the _rev3 version, power is measured every whole
      // mains cycle, so that's 50 times per second.  When integer maths is being used, this
      // stage seems unnecessary.  As all sampling periods are of similar duration (20mS), it
      // is more efficient simply to add all the power samples together, and note that their
      // sum is actually 50 times greater than it would otherwise be.
      //
      // Although the numerical value itself does not change, I think a new name would be
      // helpful so as to avoid any confusion.  The'energy' variable below is 50 * (1/powerCal)
      // times larger than the actual energy in Joules.
      //
      long realEnergy_long = realPower_long;
   
      // Energy contributions are summed in an accumulator which is generally known as the
      // energy bucket.  The purpose of the energy bucket is to mimic the operation of the
      // supply meter.  Most supply meters have a range of 0.001kWh within which energy can
      // pass to and fro without loss or charge to the user.  The energy bucket in the Mk2
      // Power Router was therefore to 0.001kWh, or 3600 Joules.  Moreover, its contents
      // were correctly pre-scaled to be in Joules.
      //
      // As described above, energy contributions for the Mk2a version are scaled somewhat
      // higher that for its floating point predecessor.  The capacity of the energy bucket for
      // Mk2a _rev3 thereore needs to be 3600J * 50 * (1/powerCal).  This is the value that
      // appears in setup().
   
   
      //----------------------------------------------------------------------------------
      // WARNING - Serial statements can interfere with time-critical code, use with care!
      //----------------------------------------------------------------------------------
/*     
      if(((cycleCount % 50) == 5) && polarityNow == POSITIVE) // display once per second
      {
        Serial.print(realPower_long);
        Serial.print(", ");
        Serial.print(energyInBucket_long);
        Serial.print(", ");
        Serial.println(energyInBucket_4led_long); // has no upper or lower limits
        energyInBucket_4led_long = 0; // useful for calibration/test purposes
      }
*/
      if (beyondStartUpPhase)
      { 
        // Providing that the initial settling time has passed,   
        // add this latest contribution to the energy bucket
        energyInBucket_long += realEnergy_long;   
        energyInBucket_4led_long += realEnergy_long;   
         
        // Apply max and min limits to bucket's level.  This is to ensure correct operation
        // when conditions change, i.e. when import changes to export, and vici versa
        //
        if (energyInBucket_long > capacityOfEnergyBucket_long)
        {
          energyInBucket_long = capacityOfEnergyBucket_long;
          digitalWrite(outputPinForLed, 1); // illuminate the on-board LED if bucket overflows
        }
        else
        {
          digitalWrite(outputPinForLed, 0); // clear the on-board LED if bucket is not overflowing
          if (energyInBucket_long < 0)
          {
            energyInBucket_long = 0;
          } 
        }
 
#ifdef TALLYMODE
        tallymode_updateData(realPower_long); // update the relevant tally
#endif
      }
      else
      { 
        // check whether the system had time to settle
        if(cycleCount > (startUpPeriod * CYCLES_PER_SECOND))
        {
          beyondStartUpPhase = true;
//        Serial.println ("go!");
        }
      }   
     
      // clear the per-cycle accumulators for use in this new mains cycle. 
      samplesDuringThisCycle = 0;
      sumP = 0;

    } // end of processing that is specific to the first Vsample in each +ve half cycle   
    else
    {     
      // This is the start of a new -ve half cycle (just after the zero-crossing point)
      //
      // This is a convenient point to update the Low Pass Filter for DC-offset removal ...
      long previousOffset = DCoffset_V_long;
      DCoffset_V_long = previousOffset + (0.01 * cumVdeltasThisCycle_long);
      cumVdeltasThisCycle_long = 0;
     
      // ... and prevent the LPF's output from drifting beyond the likely range of the voltage signal
      if (DCoffset_V_long < DCoffset_V_min) {
        DCoffset_V_long = DCoffset_V_min; }
      else 
      if (DCoffset_V_long > DCoffset_V_max) {
        DCoffset_V_long = DCoffset_V_max; }
           
      // The triac can change state at each -ve going zero crossing.  Update the state of a
      // flag which shows the current state of the triac (for anti-flicker mode logic)
      //     
      if (nextStateOfTriac == ON) {
        triacState = ON; }
      else { 
        triacState = OFF; }   
    } // end of processing that is specific to the first Vsample in each -ve half cycle
  }
 
  if (polarityNow == POSITIVE)
  {
    // for whole-cycle operation, the trigger is only armed during +ve half cycles
    if (triggerNeedsToBeArmed == true)
    {
      // check to see whether the trigger device can now be reliably armed
      if(voltageOKToArmTriggerFlag == true) 
      {
        if (operatingMode == NORMAL)
        {
          // check to see whether the energy threshold has been reached
          if (energyInBucket_long > energyThreshold_long)
          {
            nextStateOfTriac = ON; 
          }
          else
          {
            nextStateOfTriac = OFF;
          }
        }
        else
        {
          // We're in anti-flicker mode, so different logic applies ...
          //
          if (energyInBucket_long < antiFlicker_lowerEnergyThreshold)       
          {
            // when below the lower threshold, always turn the triac off
            nextStateOfTriac = OFF; 
          }
          else
          if (energyInBucket_long > antiFlicker_upperEnergyThreshold) // upper threshold     
          {
            // when above the upper threshold, ensure that the triac is on if permitted
            if (triacState != ON)
            {
              if (cycleCount > cycleCountAtLastActivation + minCycleCountsBetweenActivations)
              {
                nextStateOfTriac = ON;  // the external trigger device is active low
                cycleCountAtLastActivation = cycleCount;
              }
            }
          }
          else
          {
            // the energy level is between the upper and lower thresholds, so
            // leave the triac's state unchanged (anti-flicker measure)
          }         
        } // end of anti-flcker mode logic
                 
        // set the Arduino's output pin accordingly, and clear the flag
        digitalWrite(outputPinForTrigger, nextStateOfTriac);   
        triggerNeedsToBeArmed = false;     
      }
    }
  }
  else
  {
    // When the voltage polarity is negative, no special processing is needed.
  }

  // Rest of processing for ALL Vsamples
  //  (this has to go here because the counters / accumulators may have just been cleared)
  sumP +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
  cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
  samplesDuringThisCycle++;
}
//  ----- end of main Mk2a code -----


#ifdef TALLYMODE
void tallymode_checks()
{
  if (tallymode_firstLoop) {
    tallymode_setup(); } // user-dialogue for recording energy data
  else
  if (cycleCount >= tallymode_maxCycleCount) {
    tallymode_dispatchData(); // send recorded energy data to the Serial monitor 
//    cycleCount = 0;
    tallymode_firstLoop = true;  // get ready for another run
    pause(); } // so that user can access data from the Serial monitor

  else
  if (beyondStartUpPhase) {
      tallymode_noOfSamplePairs++; }
}


void  tallymode_setup()
{
  char inbuf[10];
  int tempInt;
  byte noOfBytes;
  boolean done;

 // <-- start of commented out section to save on RAM space.
 /*
   Serial.println ("WELCOME TO TALLYMODE ");
  Serial.println ("This mode of operation allows the energy content of individual mains cycles");
  Serial.println ("to be analysed.  For nomal operation, the #define TALLYMODE statement");
  Serial.println ("should be commented out. ");
*/
   // <-- end of commented out section to save on RAM space.

  Serial.println (); 
  Serial.println ("Tallymode setup:"); 
  Serial.print ("Time to run (secs)? ");
  done = false;
  while (!done) {
    noOfBytes = Serial.available();
    if (noOfBytes > 0) { done = true; } else { delay(100); }}
  for (tempInt = 0; tempInt < 10; tempInt++) { inbuf[tempInt] = 0; }
  Serial.readBytes(inbuf, noOfBytes);
  tempInt = atoi(inbuf);  Serial.println (tempInt);
  tallymode_maxCycleCount = tempInt * CYCLES_PER_SECOND;
  tallymode_durationOfRecording = tempInt;
 
  Serial.print ("Min value (Watts)? ");
  done = false;
  while (!done) {
    noOfBytes = Serial.available();
    if (noOfBytes > 0) { done = true; } else { delay(100); }}
  for (tempInt = 0; tempInt < 10; tempInt++) { inbuf[tempInt] = 0; }
  Serial.readBytes(inbuf, noOfBytes);
  tempInt = atoi(inbuf);  Serial.println (tempInt);
  tallymode_minVal = tempInt;
//  Serial.print(" tallymode_minVal = "); Serial.println(tallymode_minVal);   
//  tallymode_minVal_long = tallymode_minVal * (1/powerCal); // used for power, not energy,
                                                           // hence there's no x100 term.
 
  Serial.print ("Max value (Watts)? ");
  done = false;
  while (!done) {
    noOfBytes = Serial.available();
    if (noOfBytes > 0) { done = true; } else { delay(100); }}
  for (tempInt = 0; tempInt < 10; tempInt++) { inbuf[tempInt] = 0; }
  Serial.readBytes(inbuf, noOfBytes);
  tempInt = atoi(inbuf);  Serial.println (tempInt);
  tallymode_maxVal = tempInt;
//  Serial.print(" tallymode_maxVal = "); Serial.println(tallymode_maxVal);   

  if (tallymode_maxVal <= tallymode_minVal) {
    Serial.print(7); // beep
    Serial.println ("ERROR!"); }
 
  tallymode_stepVal = (float)(tallymode_maxVal - tallymode_minVal) / NUMBER_OF_TALLIES;
//  Serial.print(" tallymode_stepVal = "); Serial.println(tallymode_stepVal);   
/*
  tallymode_stepVal_long = tallymode_stepVal * (1/powerCal); // used for power, not energy,
                                                             // hence there's no x50 term.
  tallymode_halfStep = (tallymode_stepVal_long / 2); // to avoid integer-rounding
*/

  for (tempInt = 0; tempInt < NUMBER_OF_TALLIES + 2; tempInt++) {
    tally[tempInt] = 0; }
   
  energyInBucket_long = (capacityOfEnergyBucket_long / 2); // for rapid startup of power distribution
  cycleCount = 0;
  tallymode_noOfValuesTallied = 0;
  tallymode_noOfSamplePairs = 0;
  beyondStartUpPhase = false; // to allow LPF to re-settle for next run
  tallymode_firstLoop = false;

  Serial.print(" Recording will start in ");
  Serial.print(startUpPeriod);   
  Serial.println(" seconds ... ");
 
 // startTime = millis();
};


void  tallymode_updateData(long power_long)
{
  float powerInWatts = power_long * powerCal;
  int index;
 
  if (powerInWatts < tallymode_minVal)
  {
    index = 0; // tally[0] is for underflow
  }
  else
  if (powerInWatts > tallymode_maxVal)
  {
    index = NUMBER_OF_TALLIES + 1; // tally[N+1] is for overflow
  }
  else
  {
    index = (powerInWatts - tallymode_minVal) / tallymode_stepVal;
    index += 1; // because the linear tallies run from 1 to N, not from 0
  }
 
  tally[index]++; // increment the relevant tally   
  tallymode_noOfValuesTallied++;
}


void  tallymode_dispatchData()
{
 
//   <<- start of commented out section, to save on RAM space!
/*   
  Serial.println ();
  Serial.println ("Format of results: ");
  Serial.print ("Sorted data runs from tally[1] (min) to tally[");
  Serial.print (NUMBER_OF_TALLIES);
  Serial.println ("] (max).");
  Serial.print ("tally[0] is below range; tally[ ");
  Serial.print (NUMBER_OF_TALLIES + 1);
  Serial.println ("] is above range.");
  Serial.println("Tally results are displayed with max power first. For each one:");
  Serial.println("  mid-range power of tally (Watts), number of times recorded.");
  Serial.println();
*/   
//   <<- end of commented out section, to save on RAM space!
 
  Serial.println("Results for this run:");
  Serial.print (tallymode_minVal);
  Serial.println (", <- min value of tally range (W)");
  Serial.print (tallymode_maxVal);
  Serial.println (", <- max value of tally range (W)");
  Serial.print (tallymode_stepVal);
  Serial.println (", <- step value between tallies (W)");
  Serial.print (NUMBER_OF_TALLIES);
  Serial.println (", <- No. of tallies");
  Serial.print (tallymode_durationOfRecording);
  Serial.println (", <- duration (secs)");
  Serial.print (tallymode_noOfValuesTallied);
  Serial.println (", <- no of values tallied");
  Serial.print ( tallymode_noOfSamplePairs /
    ((float)tallymode_durationOfRecording * CYCLES_PER_SECOND), 1);
  Serial.println (", <- loops per mains cycle (av)");
  Serial.println("***");
 
  float powerVal;
  for (int index = NUMBER_OF_TALLIES + 1; index >= 0; index--)
  {
/* 
    // display the index value for this tally
    Serial.print (index);
    Serial.print (", ");
*/   
    // display the power value for this tally
    if (index == NUMBER_OF_TALLIES + 1)
    {
      Serial.print (">");
      Serial.print (tallymode_maxVal);
    }
    else
    if (index == 0)
    {
      Serial.print ("<");
      Serial.print (tallymode_minVal);
    }
    else 
    {
      if (index == NUMBER_OF_TALLIES)
      {
        powerVal = tallymode_maxVal - (tallymode_stepVal / 2);
      }
      else
      {
        powerVal -= tallymode_stepVal;
      }
     
      if ((int)powerVal == powerVal)
      {
        Serial.print (powerVal, 0); // to suppress the decimal part for integers
      }
      else
      {
        Serial.print (powerVal);  // non-integers are displayed to 2 dec places
      }
    } 
    Serial.print ("W");

    // display the number of hits for this tally
    Serial.print (", ");
    Serial.println (tally[index]); 
  }
};


void pause()
{
  byte done = false;
  byte dummyByte;
   
  while (done != true)
  {
    if (Serial.available() > 0) {
      dummyByte = Serial.read(); // to 'consume' the incoming byte
      if (dummyByte == 'g') done++; }
  }   
}
#endif


// helper function, to process LED events, can be conveniently called once per mains cycle
void checkLedStatus()
{
  ledState = digitalRead (ledDetectorPin);

  if (ledState != prevLedState)
  {
    // led has changed state
    if (ledState == ON)
    {
      // led has just gone on
      ledOnAt = millis();
      ledRecentlyOnFlag = true;
    }
    else
    {
      // led has just gone off   
      if (ledRecentlyOnFlag == true)
      {
        ledRecentlyOnFlag = false;       
        Serial.print ("** LED PULSE ** "); // this is a chargeable event 
      }
      else
      {
        Serial.print ("** LED OFF ** "); // 'no longer exporting' is also a chargeable event
      }
      Serial.println(millis()/1000);
    }   
  }
  else
  {
    // the LED state has not changed
    if (ledState == ON)
    {
      if (ledRecentlyOnFlag == true)
      {
        // check to see if the known duration of a pulse has been exceeded   
        unsigned long timeNow = millis();     
        if ((timeNow - ledOnAt) > 50)
        {
          Serial.print ("** LED ON **");  // 'exporting' is a non-chargeable state     
          Serial.print (",  energy in bucket = ");
          Serial.println((long)(energyInBucket_4led_long));
          ledRecentlyOnFlag = false;   
        }     
      }
    }
  }
  prevLedState = ledState;   
}
Onko sinulla netottava sähkömittari/siirto-operaattori ja jollei ole, onko operaattorin kirjaamat tuntilukemat kulutuksen ja tuoton osalta linjassa mittaamiesi käyrien kanssa? Toisekseen, millainen on puolijohdereleesi? Olen nähnyt sekä nollapistekytkimellä että vaihekulmasäädöllä toimivia "portaattomia" relehybridipiirejä. Voisi jopa ajatella sähkövastuksen ohjaamista IGBT:llä huomattavasti verkkotaajuutta suuremmalla taajuudella (luokkaa kilohertzejä). Kaksi viimeksi mainittua ratkaisua toimisivat myös ei-netotetulla sähkömittarilla toivotulla tavalla.

jolla

  • Aktiivijäsen
  • ***
  • Viestejä: 575
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #55 : 02.11.18 - klo:20:54 »
^

tuollaisia
http://www.fotek.com.hk/solid/SSR-1.htm

sähkön siirrosta vastaava operaattori lukee vain kulutuksen, ei ole netotusta....mittari, E450 lukee kylläkin kulutuksen/tuoton eri rekistereihin muttei vaiheittain

operaattorin lukemat ovat täysin linjassa inverttereiden lukemien kanssa, myös omien mittausten, menee kaheden oman erillisen mittarin läpi, landis gyr ja em111

niin, ja yksivaiheisena kaikki 'toiminta'.....3*25 päänallit nyt
« Viimeksi muokattu: 02.11.18 - klo:21:05 kirjoittanut jolla »

Harpi

  • Jäsen
  • **
  • Viestejä: 122
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #56 : 02.11.18 - klo:21:01 »
Ompa hyvä ketju tässä.

Oma järjestelmä on laajentunut tässä vuosien myötä ja alkaa olla aika monimutkainen eri variaatioilla.
Puukattila pellettipolttimolla, 1200L päävaraaja, 300L käyttövesivaraaja joka lämpenee kesäisin sähköllä.
Päävaraaja lämpenee pelletillä, tuulivoimalla tai varalla verkkosähkö. Käyttövesivaraajan tulo on päävaraajan
kautta, vastus toimii tuulivoimalla tai verkkosähköllä. Koko pakettia ohjaa logiikka eri variaatioilla.
Verkkoonsyöttävät aurinkopaneelit tuli asennettua kesällä ja nyt on aurinko-optimointi rakenteilla.
Pelletti jää kohta pois ja tilalle ehkä ilma/vesi pumppu joka pitää olla ulkoisesti ohjattavissa optimoinnin takia.
Täällä on Wattenfall/Elenia ISKRA:n mittari ja data tuosta on olematonta. Onko kukaan kokeillut ssr/pwm ohjausta tuolle?

kotte

  • Aktiivijäsen
  • ***
  • Viestejä: 1044
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #57 : 02.11.18 - klo:23:20 »
sähkön siirrosta vastaava operaattori lukee vain kulutuksen, ei ole netotusta....mittari, E450 lukee kylläkin kulutuksen/tuoton eri rekistereihin muttei vaiheittain

operaattorin lukemat ovat täysin linjassa inverttereiden lukemien kanssa, myös omien mittausten, menee kaheden oman erillisen mittarin läpi, landis gyr ja em111

niin, ja yksivaiheisena kaikki 'toiminta'.....3*25 päänallit nyt
Ovatko nuo omat mittarisi elektronisia vai mekaanisia? Mekaaninenhan vähentää tuoton kulutuksesta. Mietin sellaistakin mahdollisuutta, että jotkin mittarit saattaisivat todellakin sisältää sellaisen laitteiston ja ohjelmiston, joka ikään kuin emuloi tuollaista mekaanista mittaria, eli tuossa olisi jonkinlaista kulutuksen ja tuoton integrointia useamman vaiheen ylitse. Ratkaisu voisi olla esimerkiksi sellainen, että tehon laskenta ja integrointi (lyhyehkön jakson ylitse) tehdään analogiapiireillä ja vain kulutuksen kvantisointi ja laskenta digitaalisesti. Vedän tässä siis hiukan takaisin aikaisempien viestieni näkemystä.

En kyllä menisi luottamaan tuollaisiin ominaisuuksiin, eli jonkin toisen mittarin kohdalla tilanne saattaa olla ihan erilainen ja laskenta tehdään digitaalisesti suoraan näytteistämällä hetkittäistä jännitettä ja virtaa (tai ainakin aikavakiot ovat lyhyempiä). Jonkin uuden mittarisukupolven tapauksessa jopa saattaa kulutuksen ja tuoton netotus eri vaiheiden välillä tulla käyttöön, mutta nollapistekytkennän edellyttämällä matalalla taajuudella tehty pulssinleveysmoduloitu tehon säätö rupeaakin erottelemaan kulutukset ja tuotot puolijaksokohtaisten mittausten mukaisesti.

tengu

  • Aktiivijäsen
  • ***
  • Viestejä: 563
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #58 : 03.11.18 - klo:07:42 »

En kyllä menisi luottamaan tuollaisiin ominaisuuksiin, eli jonkin toisen mittarin kohdalla tilanne saattaa olla ihan erilainen ja laskenta tehdään digitaalisesti suoraan näytteistämällä hetkittäistä jännitettä ja virtaa (tai ainakin aikavakiot ovat lyhyempiä). Jonkin uuden mittarisukupolven tapauksessa jopa saattaa kulutuksen ja tuoton netotus eri vaiheiden välillä tulla käyttöön, mutta nollapistekytkennän edellyttämällä matalalla taajuudella tehty pulssinleveysmoduloitu tehon säätö rupeaakin erottelemaan kulutukset ja tuotot puolijaksokohtaisten mittausten mukaisesti.

Eihän tuolla ole väliä jos mittari laskee tuolla ns "energybucket" periaatteella? Ämpärissä pinta laskee ja nousee vaan vasta kun tyhjä tai tulvii niin menee mittapulssi rekisteriin.

Mahtaako olla paljonkin erilaisia sähkömittareita asennettuna eri alueille? Täällä Carunan alueella mittari ainakin pysyy hyvin nollilla, vaikka ajan täysiä puolijaksoja. Vattenfallin energywatch lukee mittarin silmää ja tuo lukee kulutukseksi niin kulutuksen kuin tuoton (jos menee verkkoon). Kun vertaan Carunan seurantaa ja energywatchia niin 0.01-0.02 kWh sisällä on samat lukemat.

Ainakin 2kWn kuormalla toimii vielä hyvin mutta en ole testannut isommalla.
3x25 tyhjiöputkea. 2,1kw PV verkossa Zeverlution 2000S. 1,2Kw PV panelia verkossa Sunny Boy 1200 SB. 240w verkossa AEConversion. 500w verkossa Envertech. 2000L varaaja+60L tulistus kv:lle. MK2PVRouter omakäytön maksimointiin.

jolla

  • Aktiivijäsen
  • ***
  • Viestejä: 575
Vs: lämminvesivaraajan tehonsäätö
« Vastaus #59 : 03.11.18 - klo:10:48 »

Ovatko nuo omat mittarisi elektronisia vai mekaanisia? Mekaaninenhan vähentää tuoton kulutuksesta. Mietin sellaistakin mahdollisuutta, että jotkin mittarit saattaisivat todellakin sisältää sellaisen laitteiston ja ohjelmiston, joka ikään kuin emuloi tuollaista mekaanista mittaria, eli tuossa olisi jonkinlaista kulutuksen ja tuoton integrointia useamman vaiheen ylitse. Ratkaisu voisi olla esimerkiksi sellainen, että tehon laskenta ja integrointi (lyhyehkön jakson ylitse) tehdään analogiapiireillä ja vain kulutuksen kvantisointi ja laskenta digitaalisesti. Vedän tässä siis hiukan takaisin aikaisempien viestieni näkemystä.

En kyllä menisi luottamaan tuollaisiin ominaisuuksiin, eli jonkin toisen mittarin kohdalla tilanne saattaa olla ihan erilainen ja laskenta tehdään digitaalisesti suoraan näytteistämällä hetkittäistä jännitettä ja virtaa (tai ainakin aikavakiot ovat lyhyempiä). Jonkin uuden mittarisukupolven tapauksessa jopa saattaa kulutuksen ja tuoton netotus eri vaiheiden välillä tulla käyttöön, mutta nollapistekytkennän edellyttämällä matalalla taajuudella tehty pulssinleveysmoduloitu tehon säätö rupeaakin erottelemaan kulutukset ja tuotot puolijaksokohtaisten mittausten mukaisesti.

tuota, tuota...enhän olekaan kokeillut tulemaa, kun irrotan säätimet, elikkä sammutan arduinot, ja annan panelien tuottaa ylimääräistä vaiheeseen ja kuormitan, vaikkapa kokeeksi manuaalisesti, kun on noita portaattomasti säädettäviä testi laitteita, toista vaihetta.....en todellakaan tiedä jospa se vaikka netottaa....kuten vanhat kiekkomittarit......no en usko mutta kokeilen
landiksen e450 välly mittaa kylläkin pulssit identtisesti molempiin suuntiin kun taasen em111 kulutuksessa välkkyy mutta toiseen suuntaan palaa koko ajan, ~10v ikäinen landis kiekko pyörii todellakin molempiin suuntiin, se olisi ollut 'kova sana' päämittarina

Ilmaisenergia.info

Vs: lämminvesivaraajan tehonsäätö
« Vastaus #59 : 03.11.18 - klo:10:48 »