PHP: Wunderground PWS from La Crosse Alerts

The code below can be reused to connect other types of weather stations, but it was originally designed to utilize undocumented features of the La Crosse Technology Wireless Weather Station with La Crosse Alerts (aka C84612).

Full tutorial is now available as a separate article “(HACKING) PROFESSIONAL WEATHER STATION FOR UNDER $100“.

Updated 2/10/2014:

  • Temp path moved to a variable instead (thank you, Mara and Ford)
  • Down alert default changed to 2 hours (La Cross internet update seems to be down just over 1 hour quite often)
  • Check for station itself being down (outside temperature reported exactly at 32 and dew point of 0)
  • Running script with ?output at the end would produce an output each time – helps troubleshoot otherwise successful runs
  • Info on delay in hours since last update always available on errors

//Unique La Crosse device id. Quickest way to get it is to login to the La Crosse website
//then go to View Source (once you see the "dashboard"). You should then see a line that
//reads "<script type="text/javascript">var PWSUser = {"device":{"id":""
//followed by a short number --- use that number as your $device variable.

//Amount of skew between true north and direction of the sensor.
//Between 1 and 359 degrees; 0 for no skew;


//Length of acceptable time since last reading from/by lacrossealerts.
//In hours (1-23).
$DownAlert = 2;

$src_tz = new DateTimeZone('America/Los_Angeles');

//Temporary path+filename to store last-update info
$temppath = "/tmp/weather";

$dest_tz = new DateTimeZone('UTC');
$currentTime=new DateTime("now", $dest_tz);

$url = '';
$fields = array('username' => urlencode($username),'password' => urlencode($password));
$json = curl_grab_page($url,$fields, null, $urlgrab);
$obj = json_decode($json, true);
switch ($err) {
//echo ' - No errors';
ErrorDie('Maximum stack depth exceeded',$json);
ErrorDie('Underflow or the modes mismatch', $json);
ErrorDie('Unexpected control character found', $json);
ErrorDie('Syntax error, malformed JSON', $json);
ErrorDie('Malformed UTF-8 characters, possibly incorrectly encoded', $json);
ErrorDie('Unknown error: ' .$err, $json);
if($status==null || $status!="true")
ErrorDie('Unable to parse, unsuccess response', $json);

if($obj==null || count($obj)<1)
ErrorDie('Unable to parse (response:obs missing)', $json);
ErrorDie('Unable to parse (time missing)', $obj, $json);

$dt = new DateTime($time, $src_tz);
$time=$dt->format('Y-m-d H:i:s');
$fetchDelay=date_diff($dt, $currentTime);
$HourFetchDelay = ($fetchDelay->y * 365.25 + $fetchDelay->m * 30 + $fetchDelay->d) * 24 + $fetchDelay->h + $fetchDelay->i/60;

$windDirection =$obj["values"]["outdoor"]["windDirection"];
$windSpeed =$obj["values"]["outdoor"]["windSpeed"];
$windGust =$obj["values"]["outdoor"]["windGust"];

$temp =$obj["values"]["outdoor"]["temp"];
$rain1hr =$obj["values"]["outdoor"]["rain1hr"];
$rain24hr =$obj["values"]["outdoor"]["rain24hr"];
$rh =$obj["values"]["outdoor"]["rh"];
$dewpoint =$obj["values"]["outdoor"]["dewpoint"];
if($temp=="" || ($temp=="32" && $dewpoint=="0"))
ErrorDie('Outside temperature of 32 and dew point of 0 - used when La Crose station values are not available', $obj, $json);

$tempi =$obj["values"]["indoor"]["temp"];
$rhi =$obj["values"]["indoor"]["rh"];


$ch = curl_init($pushURL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$submitted = curl_exec($ch);
curl_close ($ch);
if(strpos($submitted, "success") !== FALSE){
//echo ' - No errors';
if (!$handle = fopen($temppath, 'w')) {
ErrorDie("Cannot open temp file for writing.");
if (fwrite($handle, $time) === FALSE) {
ErrorDie("Cannot write last update time to the temp file.");

ErrorDie("Updated successfully, however it's been ".number_format((float)$HourFetchDelay, 2, '.', '')." hours since last fetch of data from the station!");
elseif(strpos($submitted, "INVALIDPASSWORDID") !== FALSE)
ErrorDie('Invalid user data entered in the ID and PASSWORD GET parameters', $json, $obj, $pushURL);
elseif(strpos($submitted, "RapidFire Server") !== FALSE)
ErrorDie('The minimum GET parameters ID, PASSWORD, action, and dateutc were not set', $json, $obj, $pushURL);
ErrorDie('Unrecognized response: '. $submitted, $json, $obj, $pushURL);

if(isset($_GET["output"])) {
echo "<pre>";
echo "</pre><br>";
echo str_replace($station,"",str_replace($stationpass,"",$pushURL));

function ErrorDie($message=null, $object=null, $object2=null, $object3=null) {
global $dest_tz, $currentTime, $DownAlert,$temppath;
echo "Output mode set- outputting errors in real-time\r\n";
//if still within the time-frame, don't notify of the error
else {
if (!$handle = fopen($temppath, "r")) {
echo "Cannot read last time from the temp file\r\n";
else {
if(($buffer = fgets($handle, 4096)) !== false) {
$dt = new DateTime($buffer,$dest_tz);
if (!feof($handle)) {
echo "Error: unexpected fgets() fail\r\n";
$fetchDelay=date_diff($dt, $currentTime);
$HourFetchDelay = ($fetchDelay->y * 365.25 + $fetchDelay->m * 30 + $fetchDelay->d) * 24 + $fetchDelay->h + $fetchDelay->i/60;
echo "It's been ".number_format((float)$HourFetchDelay, 2, '.', '')." hours since last fetch/update.\r\n";

echo $message;
//echo "</pre>";

function deSkewDirection($originalDeg, $skewDeg) {
if($skewDeg<1 || $skewDeg>359)
return $originalDeg;
$newDeg = $originalDeg+$skewDeg;
return $newDeg;
return $newDeg-360;

// $url = page to POST data
// $proxy = proxy data
function curl_grab_page($url,$fields,$proxy,$urlgrab){
$tmpfname = tempnam("/tmp", "cookie");
$handle = fopen($tmpfname, "w");

foreach($fields as $key=>$value) { $fields_string .= $key.'='.$value.'&'; }
rtrim($fields_string, '&');

$ch = curl_init();
curl_setopt($ch, CURLOPT_COOKIEJAR, $tmpfname);
curl_setopt($ch, CURLOPT_COOKIEFILE, $tmpfname);

//Uncool, but one less thing to jump at you from the logs
$browsers = array("Mozilla/5.0 (X11; U; Linux i686; en-US; rv: Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20060918 Firefox/2.0", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv: Gecko/2008092417 Firefox/3.0.3", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)");
$choice2 = array_rand($browsers);
$browser = $browsers[$choice2];
curl_setopt($ch, CURLOPT_USERAGENT, $browser);

curl_setopt($ch, CURLOPT_TIMEOUT, 10);
if ($proxy != null) {
curl_setopt($ch, CURLOPT_PROXY, $proxy);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_REFERER, $url);

curl_setopt($ch, CURLOPT_VERBOSE, FALSE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_POST, TRUE);

if(count($fields)>0) {
curl_setopt($ch,CURLOPT_POST, count($fields));
curl_setopt($ch,CURLOPT_POSTFIELDS, $fields_string);
if(!$result = curl_exec($ch))
curl_close ($ch);
ErrorDie("Fatal error while authenticating.", $err);

curl_setopt($ch, CURLOPT_URL, $urlgrab);
$return = curl_exec ($ch); // execute the curl command
curl_close ($ch);


return $return;
  • divinemayhemstudios

    I’m getting an error on line 39 – $err=json_last_error();
    The error reads: (** used to remove my personal info)
    [21-Feb-2014 13:30:07] PHP Fatal error: Call to undefined function json_last_error() in /**/**/public_html/Weather_Station/PWS.php on line 39

    I have the PHP on my webhost in a folder named Weather_Station.
    I have the temppath set up as Weather_Station/tmp/weather/
    The comments mention Temporary path+filename to store last-update info. How do I indicate the filename on that line? what extension should be used (if any)?

    • The undefined function error sounds like version of the PHP you are running may be prior to PHP 5.3 (that’s when the function was introduced), is that the case? If so, is there any way to have your webhost upgrade it?

      As far as the path, simply have the name at the end of the variable with no slash at the end (you may want to have one in the beginning, however, if it’s an absolute path). In the sample code I have “/tmp/weather”, which means that it would be placed into the root folder “tmp”, file “weather” (in this case the file won’t have an extension, which is fine). Also note that I’m using a true, preexisting, path that I do have access to- please make sure that your path exists as well (tmp in root is usually a good bet for unix, especially since there is nothing secure being placed in the file, simply last fetch date-time).

      • divinemayhemstudios

        Bluehost, my webhost, offers a selection of versions to use. I am using 5.4. The list of what they offer is linked in the attached image.

        My .htaccess file is set up for 5.4 as well.
        # Use PHP5.4 as default
        AddHandler application/x-httpd-php54 .php
        AddType application/x-httpd-php .html .htm

        As far as the path, I have a folder structure laid out as described
        in my first comment, /public_html/Weather_Station/tmp/weather. There is
        also a tmp folder above /public_html/ that I have added a folder named
        weather, just in case it would like to use that. Is there a better way
        to use relative path notation to make it use the /tmp/weather folder
        under the /Weather_Station folder?

        Does it matter?

        • divinemayhemstudios

          Oh.. the last line on the image states that I need to make sure that my CRONs use the correct PHP.. Trying that and will report back.

          • divinemayhemstudios

            I’m awaiting a reply from tech support on how to make sure that my cron
            job uses PHP 5.4. 2 chats with support persons haven’t yielded a

          • Let me know how that goes- that sounds exactly like the issue- you would need to specify that PHP version path directly, as in their instructions, followed to the path to the script… That should do the trick.

          • divinemayhemstudios

            It works! I had to set up a cron job that makes sure that I am utilizing PHP 5.4 and runs your (user corrected) PHP file, but it works just fine.
            I really appreciate all of the work that you’ve put into this.

          • Absolutely thrilled to hear that!
            Enjoy and let me know if you think of any improvements, changes, or additions.

        • As far as the path– if you DO want it in the publicly accessible from the web “tmp” folder, based on your comments the variable you would want to use is:
          $temppath = “/public_html/Weather_Station/tmp/weather”;

  • Cayuse

    Works great so far for my station. The only downside seems to be that doesn’t update as often as it gets new files so wunderground data is sometimes stale. When I download the .csv from it shows file updates every 15 minutes with different data but my page doesn’t always reflect the new data and therefore it doesn’t go through to

    • Great to hear that it’s working for you!

      It’s unfortunate about the delays with their systems. There is actually an unrelated ongoing project called SkySpy that I was recently made aware of- its purpose is to replace LaCross hardware/site completely and, if it pans out, merging these two projects would lead to true real-time Wunderground updates, so I’m really hoping for it…

      I’ve looked at the CSV previously, but what I’m seeing is even worse data there- the stamps there are 2 hours apart even when my station is getting proper updates. Is that not what you are seeing??

      • Cayuse

        On under the station settings you can edit the device setting for your station and set a history interval. I have mine set at 15 minutes and find that the csv file shows an update every 15 minutes even though the website doesn’t post the update. Sometimes the website goes happily along and other times it can be several hours between posted updated but the data still exists in the csv.

        • Interesting- that’s what mine is set to, but it seems to not have any effect (16:00->18:00 and so on)… Can you share your last CSV?
          We can definitely make the script configurable to look at *either* location, if that would help at least some users.

          • Cayuse

            Where can I send to? Disqus only allows .jpg uploads
            2/25/2014 14:30 0 0 70.7 28 33.98 55 11.48 0.92 2.65 180 0 30.19 0
            2/25/2014 14:45 0 0 70.52 27 34.34 55 11.84 0.46 1.38 112.5 0 30.18 0
            2/25/2014 15:00 0 0 70.7 28 34.88 55 12.38 0.69 1.38 337.5 0 30.18 0
            2/25/2014 15:15 0 0 70.7 29 34.88 53 11.12 1.38 2.65 202.5 0 30.18 0
            2/25/2014 15:30 0 0 70.52 30 35.24 55 12.74 0.69 1.38 157.5 0 30.18 0
            2/25/2014 15:45 0 0 70.7 30 35.06 55 12.56 0.69 1.38 157.5 0 30.17 0
            2/25/2014 16:00 0 0 70.34 30 35.06 56 13.28 1.84 2.65 112.5 0 30.16 0
            2/25/2014 16:15 0 0 70.16 30 35.06 57 13.82 0.46 1.38 112.5 0 30.16 0
            2/25/2014 16:30 0 0 70.34 30 34.88 56 13.1 0.46 1.38 135 0 30.16 0
            2/25/2014 16:45 0 0 70.16 30 34.52 56 12.74 0.23 1.38 180 0 30.15 0
            2/25/2014 17:00 0 0 69.8 30 34.34 57 13.28 0 0 315 0 30.16 0
            2/25/2014 17:15 0 0 69.44 30 33.98 57 12.92 0 0 90 0 30.15 0
            2/25/2014 17:30 0 0 69.08 30 33.44 58 12.92 0 0 292.5 0 30.15 0
            2/25/2014 17:45 0 0 68.9 29 32.54 59 12.74 0 0 67.5 0 30.15 0
            2/25/2014 18:00 0 0 68.54 29 32 59 12.2 0 0 247.5 0 30.15 0
            2/25/2014 18:15 0 0 68.18 29 31.28 60 12.2 0 0 292.5 0 30.15 0

          • Got it– looks like in your case the settings applied 100%…but I wonder if it limits to just the output or actual delays in reporting as well. If you browse to the following URL (where XXXXX is your device id), does it show a different (older) timestamp than the last one in the official CSV output you get off the website?

          • Cayuse

            Frequently it does show a timestamp much older than the most recent one in the csv, sometimes by several hours. Not sure why it doesn’t refresh more regularly. Today there seems to be some sort of issue where things aren’t loading properly that I will need to try and sort out when I get home.

          • Cayuse

            I’ve switched to using Skyspy from in this thread right now it updates consistently every 8 or 9 minutes but hopefully will be getting down into the 2 minute range soon. Thanks for your development efforts.

          • That’s great! I’m glad K. (@skydvrz) was able to merge this into his awesome project and release the app out.
            We’ve been in minor communications behind the scenes, but unfortunately I have not been able to put much time into it just yet (sorry, K!). Ultimately I’m hoping that both/either of us find some time to take it yet to the next level (cloud-hosted server independent of Windows and/or any local machine).
            Thank you for the update (and introducing @skydvrz to this effort?) and please do keep me posted as things change!

  • AudubonCreek

    Hello Ilya. Have you updated the code to work with Lacrosse Alerts Mobile? Lacrosse no longer supports Lacrosse alerts.

    • Thank you for your comment AudubonCreek! Unfortunately I have actually moved on from this system (too many reporting issues and glitches) and switched to another set (also from Costco) by AcuRite. Switching out the format and URLs should not be too hard, however, let me know if you attempt and if you run into any issues at all, I would be happy to assist.

      • AudubonCreek

        Thank you for offering to help, Ilya. My last programming experience was in Business Basic in 1982, so, I’m an old man that is lost. 🙂

        I’ve entered this info:

        $username=”[email protected]”;
        $src_tz = new DateTimeZone(‘America/Chicago’);

        I also changed the URL:
        $url = ‘’;
        $urlgrab = ‘’:

        And I get this error:
        Parse error: syntax error, unexpected T_DNUMBER in /home/content/a/u/d/auduboncreek/html/WX-Poll.php on line 12

        • This most likely means that the response (not surprisingly) has changed — try hitting their URLs with your credentials and see what kind of response (format) they send back — you will need to adjust the code to look at potentially different fields… Let me know if this makes sense.

          • AudubonCreek

            Due to my very limited knowledge and experience, I am not sure what you are asking me, Ilya. I am able to login using ‘[email protected]’ with the password ‘tmppwd01’.