Help with Scrape Sensor since 0.86 changes

Since 0.86, there have been changes to fix a bug.
Unfortunately it also means that I can no longer scrape the battery value from my fire tablet via the Fully Kiosk app.
My scrape sensor is:

- platform: scrape
  resource: !secret fire7url
  name: Fire7 Battery
  select: "td:nth-of-type(44)"
  value_template: '{{ value.split("%")[0] }}'
  unit_of_measurement: '%'

it used to extract the battery value from this page:

The sensor now comes with a “unknown” value

and I get lots of error messages in my log:

2019-01-28 18:48:11 ERROR (SyncWorker_6) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:48:15 ERROR (SyncWorker_5) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:48:19 ERROR (SyncWorker_0) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:48:38 ERROR (SyncWorker_9) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:48:39 ERROR (SyncWorker_2) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:48:50 ERROR (SyncWorker_0) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:49:10 ERROR (SyncWorker_0) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:49:23 ERROR (SyncWorker_9) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:49:27 ERROR (SyncWorker_3) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:49:41 ERROR (SyncWorker_6) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:49:46 ERROR (SyncWorker_2) [homeassistant.components.sensor.scrape] Unable to extract data from HTML
2019-01-28 18:49:53 ERROR (SyncWorker_0) [homeassistant.components.sensor.scrape] Unable to extract data from HTML

when setting the scrape sensor to debug, I can see the full html content:

Click to expand log
2019-01-28 15:56:53 DEBUG (SyncWorker_6) [homeassistant.components.sensor.scrape] <!DOCTYPE html>

<html>
<head>
<meta charset="utf-8"/>
<meta content="width=device-width,initial-scale=1" name="viewport"/>
<title>Fully Remote Admin</title>
<link href="fully-favicon.ico" rel="shortcut icon" type="image/x-icon"/>
<style>
* { margin: 0; padding: 0; }
body { font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; text-align:left; box-sizing:border-box; overflow-x:hidden; min-height:100%; }
h1 { font-size: 1.35em; margin-bottom: 0.7em;} 
h2 { font-size: 1em; } 
img.header { max-width:100%; }
img.screenshot, #imgholder { max-width:100%; border:2px solid #666666; margin-top:0.2em; margin-bottom:0.4em; text-align:center; }
#imgholder { display:none; min-width:100%; min-height:195px;}
form, input, textarea, p, div { font-size: 1.0em; }
p  {margin-bottom: 1em;}
p.buttonline  {margin-bottom: 0em; line-height: 1.5em;}
.small { font-size: 0.7em; }
.key { display: none; }
a, a:visited {color: #304ffe; text-decoration:none;}
.disabled { color: #808080; }

p.error, p.success, p.saving { line-height:1.5em; margin-top:0.5em; }
p.error, p.success { margin-left:-1.8em; margin-right:-1.8em; padding-left:1.8em; padding-right:1.8em; }
.table-cell p.error, .table-cell p.success, .table-value p.error, .table-value p.success {  margin-left: -0.5em;  margin-right: -0.5em; padding-left:1.2em; padding-right:1.2em; }
p.error { color: #FFFFFF; background:#FF0000; border-top:2px solid #880000;border-bottom:2px solid #880000;}
p.success { color: #FFFFFF; background:#00CC00; border-top:2px solid #007700;border-bottom:2px solid #007700;}

table.table { width:100%; background-color:#eee; border:1px solid #666666; border-spacing:0.2em; margin-bottom:0.5em; }
table.spaceafter { margin-bottom:1.5em;}
tr.table-row { }
td.table-cell { background-color:#ddd; padding:0.3em; margin:0.1em; vertical-align:top; }
td.table-head { background-color:#bbb; padding:0.3em; vertical-align:top; font-weight: bold;}
td.table-value { background-color:#ccc; padding:0.3em; vertical-align:top; text-align:right;}

.editArea {
  margin-top: 0.4em;
  margin-bottom: 0.4em;
  display: none;
}

.smallicon {
  height: 0.8em;
  width: 0.8em;
}

.button, .smallbutton {
  color: #ffffff;
  background: #37474f;
  font-size: 1.00em;
  font-weight: bold;
  border: 1px solid #808080;
  height: 2em;
  padding-left: 1em;
  padding-right: 1em;
  cursor: pointer;
  white-space: nowrap;
}

.button:hover, .smallbutton:hover {
  color: #ffffff;
  background: #6a6a6a;
}

a.button, .smallbutton {
  color: #ffffff;
  font-size: 0.9em;
  height: 1.5em;
  padding-left: 0.5em;
  padding-right: 0.5em;
  text-decoration:none;
  cursor: pointer;
  white-space: nowrap;
}

.formline, .formlinesmall, textarea {
  border: 1px solid #37474f;
  min-height: 2em;
  padding-left: 0.2em;
  font-size: 0.9em;
}

.formlinesmall {
  height: 1.7em;
  font-size: 0.8em;
}

.content {
  width: 90%;
  min-height:100%;
  max-width:40em;
  margin: 0 auto;
  transition: 0.5s;
  padding: 5.5em 1.8em 1.8em;
  background: #ffffff;
  box-shadow: 0 0 1.2em rgba(0,0,0,0.5);
  position:absolute;
  top: 0;
  left: 0;
}

@media (min-width:55em) {
    .banner {
      margin: 0 auto;
      /*transition: 0.5s;*/
      padding: 5.5em 1.8em 1.8em;
      background: #ffffff;
      position:absolute;
      top: 0;
      left: 43em;
    }
}

@media (max-width:55em) {
    .banner {
      display:none;
    }
}

.overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  background: rgba(0,0,0,0.5);
  z-index: 1;
}

#hamburger-checkbox {
  display: none;
}

label.hamburger {
  position: fixed;
  width: 3.8em;
  height: 3.8em;
  cursor: pointer;
  text-transform: uppercase;
  font-weight: 700;
  z-index: 999;
}

label.hamburger span {
  display: block;
  top: 8px;
  width: 1.5em;
  height: 0.3em;
  background-color: #ffffff;
  position: relative;
  /*position: absolute;*/
  top: 1.6em;
  left: 1.1em;
  -webkit-transition-duration: 0;
  -moz-transition-duration: 0;
  -ms-transition-duration: 0;
  -o-transition-duration: 0;
  transition-duration: 0;
  -webkit-transition-delay: 0.2s;
  -moz-transition-delay: 0.2s;
  -ms-transition-delay: 0.2s;
  -o-transition-delay: 0.2s;
  transition-delay: 0.2s;
}
label.hamburger span::after, label.hamburger span::before {
  display: block;
  content: '';
  position: absolute;
  width: 1.5em;
  height: 0.3em;
  background-color: #ffffff;
  -webkit-transition-property: margin, -webkit-transform;
  -webkit-transition-duration: 0.2s;
  -moz-transition-duration: 0.2s;
  -ms-transition-duration: 0.2s;
  -o-transition-duration: 0.2s;
  transition-duration: 0.2s;
  -webkit-transition-delay: 0.2s, 0;
  -moz-transition-delay: 0.2s, 0;
  -ms-transition-delay: 0.2s, 0;
  -o-transition-delay: 0.2s, 0;
  transition-delay: 0.2s, 0;
}

label.hamburger span::before {
  margin-top: -0.5em;
}
label.hamburger span::after {
  margin-top: 0.5em;
}

#hamburger-checkbox:checked ~ label.hamburger span {
  background-color: transparent;
}
#hamburger-checkbox:checked ~ label.hamburger span::before,
#hamburger-checkbox:checked ~ label.hamburger span::after {
  margin-top: 0px;
  -webkit-transition-delay: 0, 0.2s;
  -moz-transition-delay: 0, 0.2s;
  -ms-transition-delay: 0, 0.2s;
  -o-transition-delay: 0, 0.2s;
  transition-delay: 0, 0.2s;
}
#hamburger-checkbox:checked ~ label.hamburger span::before {
  -webkit-transform: rotate(45deg);
  -moz-transform: rotate(45deg);
  -ms-transform: rotate(45deg);
  -o-transform: rotate(45deg);
  transform: rotate(45deg);
}
#hamburger-checkbox:checked ~ label.hamburger span::after {
  -webkit-transform: rotate(-45deg);
  -moz-transform: rotate(-45deg);
  -ms-transform: rotate(-45deg);
  -o-transform: rotate(-45deg);
  transform: rotate(-45deg);
}

#hamburger-checkbox:checked ~ nav.off-canvas-menu {
  -ms-transform: translateX(0);
  -webkit-transform: translateX(0);
  transform: translateX(0);
  transition: 0.5s;
}

#hamburger-checkbox:checked ~ .content {
  -ms-transform: translateX(11em);
  -webkit-transform: translateX(11em);
  transform: translateX(11em);
  transition: 0.5s;
}

#hamburger-checkbox:checked ~ .overlay {
  height: 100%;
  opacity: 1;
}

nav li,
label.hamburger {
  transition: 0.2s;
}

nav li:hover,
label.hamburger:hover,
#hamburger-checkbox:checked ~ label.hamburger {
  background: #207ce5 !important;
}

.hidden {
  display: none;
}

.main-menu {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3.8em;
  background: #37474f;
  box-shadow: 0 0 10px rgba(0,0,0,0.9);
  z-index: 3;
}

.main-menu header {
  font-size: 1.35em;
  font-weight: bold;
  margin-left: 4em;
  margin-top: 0.7em;
  white-space: nowrap;
}

.main-menu header a {
  text-decoration:none;
  color: #ffffff;
}

.main-menu header a:visited {
  color: #ffffff;
}

.main-menu ul.nav-icons {
  float: right;
}

.main-menu li {
  float: left;
  line-height: 3.8em;
  list-style: none;
  transition: 0.3s;
}

.main-menu li a {
  display: inline-block;
}

.main-menu li i {
  width: 3.8em;
  font-size: 1em;
  color: #ffffff;
  text-align: center;
  text-decoration: none;
  vertical-align: middle;
}

.off-canvas-menu {
  position: fixed;
  top: 0;
  left: 0;
  width: 11em;
  height: 100%;
  background: #37474f;
  font-size: 1em;
  -ms-transform: translateX(-11em);
  -webkit-transform: translateX(-11em);
  transform: translateX(-100%);
  box-shadow: 0 0 10px rgba(0,0,0,0.9);
  transition: 0.5s;
  z-index: 2;
}

.off-canvas-menu input[type=checkbox] {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  display: block;
  cursor: pointer;
}

.off-canvas-menu ul {
  margin: 0;
  padding: 0;
}

.off-canvas-menu > ul {
  margin-top: 3.8em;
}

.off-canvas-menu a {
  display: block;
  padding: 1.0em 1.2em;
  color: #fff;
  text-decoration: none;
}

.off-canvas-menu li {
  position: relative;
  float: left;
  width: 100%;
  list-style: none;
  color: #ffffff;
  transition: 0.5s;
  border-top: 1px solid #555;
}

.off-canvas-menu > ul > li:last-child {
  border-bottom: 1px solid #555;
}

.off-canvas-menu ul li:first-child {
  border-top: none;
}

.off-canvas-menu ul > li.sub > a:after {
  position: relative;
  float: right;
  content: '+';
  font-size: 1.5em;
  font-weight: 700;
  color: #ffffff;
  vertical-align: middle;
  transition: 0.5s;
}

.off-canvas-menu .submenu {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s ease-in-out;
  border-top: none;
}

.off-canvas-menu input[type=checkbox]:checked ~ .submenu {
  border-top: 1px solid #555;
  max-height: 999px;
}

.off-canvas-menu input[type=checkbox]:checked ~ a:after {
  transform: rotate(45deg);
}

.off-canvas-menu .submenu li {
  background: #333;
}

.off-canvas-menu .submenu li a {
  padding-left: 30px;
}

.off-canvas-menu .submenu li li a {
  padding-left: 35px;
}

.off-canvas-menu .submenu li li li a {
  padding-left: 40px;
}

.off-canvas-menu .submenu li.sub {
  list-style: none;
}


</style>
<script defer="" src="jquery.min.js"></script>
<script>
function showClass(className) {
    var elements = document.getElementsByClassName(className), i;
    for (var i = 0; i < elements.length; i ++) elements[i].style.display = "inline";
}
function askAndLoadUrl() {
    var url=prompt("Enter URL to show in Fully","");
    if (url!=null)
        window.location = "?cmd=loadURL&url="+encodeURIComponent(url);
}
function askAndLoadZip() {
    var url=prompt("Enter ZIP file URL to load and unpack to /sdcard. Be careful, existing files will be overwritten!","");
    if (url!=null)
        window.location = "?cmd=loadZipFile&url="+encodeURIComponent(url);
}
function askAndLoadApk() {
    var url=prompt("Enter APK file URL to load and install. Upgrade from APK will fail if the app was installed from Google Play. No downgrade is possible on this way. ATTENTION: Fully WILL STOP and user input is required ON THE DEVICE in order to install the APK file!!!","");
    if (url!=null)
        window.location = "?cmd=loadApkFile&url="+encodeURIComponent(url);
}
function toggleImage(url) {
    var image = document.getElementById("imgholder");
    if (image.style.display == "none" || image.style.display == "" || image.src.indexOf(url)<0) {
        image.style.display = "block";
        $("#refreshbutton").show();

        var img = new Image(); // Preload
        img.src = url + "&time="+new Date().getTime();

        img.addEventListener('load', function () {
           var image = document.getElementById("imgholder");
           image.src = this.src;
        }, false);
    }
    else {
        image.style.display = "none";
        $("#refreshbutton").hide();
    }
}
function refreshImage(url) {
    $("#refreshbutton").hide();
    if (!document.images) return;
    var img = new Image();
    img.src = url + "&time="+new Date().getTime();

    img.addEventListener('load', function () {
       var image = document.getElementById("imgholder");
       image.src = this.src;
       setTimeout('refreshImage(\''+url+'\')', 100); // refresh after the next 100ms
    }, false);
}

function toggleVisibility(id) {
    var element = document.getElementById(id);
    if (element.style.display == "none" || element.style.display == "")
        element.style.display = "block";
    else
        element.style.display = "none";
}

function submitForm(key) {

    if (!window.jQuery) {
        setTimeout(function() { submitForm(key); }, 200);
        return;
    }

  	var markerId = "marker-"+Math.floor((Math.random() * 100000) + 1);
    var form = $("#form-"+key);
    var area = $("#edit-"+key);
    var value = $("#value-"+key);
    var newValue = "";

    if (form.find('input[name="value"]').attr('type')=='radio')
        newValue = form.find('input[name="value"]:checked').attr('customValue');
    else if (form.find('textarea[name="value"]').length) {
        if (form.find('textarea[name="value"]').attr('type')=='json')
            newValue = "(JSON)";
        else
            newValue = form.find('textarea[name="value"]').val();
    }
    else if (form.find('input[name="value"]').attr('type')=='password')
        newValue = "*****";
    else
        newValue = form.find('input[name="value"]').val();

  	console.log(" Form data: "+form.serialize());
  	console.log(" New value: "+newValue);

    area.hide();
    area.after("<div id='"+markerId+"' class='markerarea'><p class='saving'>Saving... </p></div>");
    marker = $("#"+markerId);

   	$.ajax({
      dataType: "json",
      url: "?type=json",
      method: "POST",
      data: form.serialize()})

	  .done(function( json ) {
	    console.log( "JSON Status: " + json.status+", "+json.statustext );
	    if (json.status == "OK") {
	    	value.html(htmlEncode(newValue));
	    	marker.html("<p class='success'>"+htmlEncode(json.statustext)+"</p>");
	    }
	    else if (json.status == "Error") {
	    	marker.html("<p class='error'>"+htmlEncode(json.statustext)+"</p>");
	    }
	    else {
	    	marker.html("<p class='error'>Error communicating with device</p>");
	    }
	    marker.delay(1500).fadeOut(300, function() { marker.hide('slow', function(){ marker.remove(); }); });
	  })

	  .fail(function( jqxhr, textStatus, error ) {
	    console.log(" Request Failed: " + textStatus + ", " + error);
	    console.log(" Response: "+ jqxhr.responseText);
	    marker.html("<p class='error'>Error communicating with device</p>");
	    marker.delay(1500).fadeOut(300, function() { marker.hide('slow', function(){ marker.remove(); }); });
      }
    );
}

function htmlEncode(value){
  return $('<div/>').text(value).html().replace(/\n/g,"<br>");
}

function htmlDecode(value){
  return $('<div/>').html(value).text();
}
</script>
</head>
<body>
<input id="hamburger-checkbox" type="checkbox"/>
<label class="hamburger" for="hamburger-checkbox"><span></span></label>
<div class="overlay"></div>
<div class="banner">
<a href="?cmd=home"><img class="header" src="fully-header-web-90.png"/></a>
</div><nav class="main-menu"><header><a href="?cmd=home">Fully Remote Admin </a></header></nav><nav class="off-canvas-menu"><ul>
<li><a href="?cmd=deviceInfo" title="">Device Info</a></li>
<li><a href="?cmd=listSettings" title="">Settings</a></li>
<li><a href="?cmd=manageSettings" title="">Export/Import</a></li>
<li><a href="?cmd=logout" title="Logout">Logout</a></li>
</ul></nav>
<div class="content">
<h1>Fully Info</h1>
<img id="imgholder" src="fully-loading.png"/>
<table class="table spaceafter">
<tr class="table-row"><td class="table-cell">Fully Device ID</td><td class="table-cell">[REDACTED]</td></tr>
<tr class="table-row"><td class="table-cell">Start URL</td><td class="table-cell">https://192.168.0.24:8123/lovelace/13?kiosk <a class="button" href="?cmd=loadStartURL">Load it</a> <a class="button" href="javascript:askAndLoadUrl();">Load other URL</a></td></tr>
<tr class="table-row"><td class="table-cell">Current page</td><td class="table-cell">https://192.168.0.24:8123/lovelace/13?kiosk <a class="button" href="javascript:toggleImage('?cmd=getScreenshot');">Screenshot</a> <a class="button hidden" href="javascript:refreshImage('?cmd=getScreenshot');" id="refreshbutton" title="Auto-reload the screenshots as often as possible (experimental)">Auto play</a></td></tr>
<tr class="table-row"><td class="table-cell">Maintenance mode</td><td class="table-cell">off <a class="button" href="?cmd=enableLockedMode">Lock for maintenance</a></td></tr>
<tr class="table-row"><td class="table-cell">Kiosk mode</td><td class="table-cell">on</td></tr>
<tr class="table-row"><td class="table-cell">Motion detection</td><td class="table-cell">on <a class="button" href="javascript:toggleImage('?cmd=getCamshot');">Cam shot</a></td></tr>
<tr class="table-row"><td class="table-cell">Acoustic detection</td><td class="table-cell">off</td></tr>
<tr class="table-row"><td class="table-cell">Movement detection</td><td class="table-cell">off</td></tr>
<tr class="table-row"><td class="table-cell">Device admin</td><td class="table-cell">on</td></tr>
<tr class="table-row"><td class="table-cell">Last App Start</td><td class="table-cell">28/01/2019 2:00:12 pm <a class="button" href="?cmd=restartApp">Restart App</a></td></tr>
<tr class="table-row"><td class="table-cell">Active fragment</td><td class="table-cell">screensaver <a class="button" href="?cmd=popFragment">Back</a></td></tr>
<tr class="table-row"><td class="table-cell">Fully version</td><td class="table-cell">1.28.1-fire <a class="button" href="javascript:askAndLoadApk();">Install APK file</a></td></tr>
<tr class="table-row"><td class="table-cell">Webview UA</td><td class="table-cell"><div title="Mozilla/5.0 (Linux; Android 5.1.1; KFAUWI Build/LVY48F; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Safari/537.36">Mozilla/5.0 (Linux; Android 5.1.1; KFAUWI Build/LVY48F;...</div></td></tr>
<tr class="table-row"><td class="table-cell">App Code/Data/Cache</td><td class="table-cell">?/?/? KB <a class="button" href="?cmd=clearWebstorage">Clear webstorage</a> <a class="button" href="?cmd=clearCache">Clear cache</a> <a class="button" href="?cmd=clearCookies">Clear cookies</a></td></tr>
<tr class="table-row"><td class="table-cell">App RAM (used/free)</td><td class="table-cell">11830/86473 KB</td></tr>
</table>
<h1>Device Info</h1>
<table class="table">
<tr class="table-row"><td class="table-cell">Device Name</td><td class="table-cell">Fire</td></tr>
<tr class="table-row"><td class="table-cell">Hostname</td><td class="table-cell">192.168.0.11</td></tr>
<tr class="table-row"><td class="table-cell">IP4 Address</td><td class="table-cell">192.168.0.11</td></tr>
<tr class="table-row"><td class="table-cell">IP6 Address</td><td class="table-cell">FD18:1A6C:BEE8:1:4600:49FF:FE51:AF2D</td></tr>
<tr class="table-row"><td class="table-cell">Mac Address</td><td class="table-cell">[REDACTED]</td></tr>
<tr class="table-row"><td class="table-cell">Wifi SSID</td><td class="table-cell">"[REDACTED]"</td></tr>
<tr class="table-row"><td class="table-cell">Battery level</td><td class="table-cell">61% (plugged)</td></tr>
<tr class="table-row"><td class="table-cell">Screen brightness</td><td class="table-cell">0</td></tr>
<tr class="table-row"><td class="table-cell">Screen status</td><td class="table-cell">on <a class="button" href="?cmd=screenOff">Turn off</a></td></tr>
<tr class="table-row"><td class="table-cell">Keyguard locked</td><td class="table-cell">off</td></tr>
<tr class="table-row"><td class="table-cell">Foreground app</td><td class="table-cell"> <a class="button" href="?cmd=toForeground">Get focus</a></td></tr>
<tr class="table-row"><td class="table-cell">Total RAM (used/free)</td><td class="table-cell">627096/285120 KB <a class="button" href="javascript:askAndLoadZip();">Upload ZIP file</a></td></tr>
<tr class="table-row"><td class="table-cell">Screen</td><td class="table-cell">600x1024 px</td></tr>
<tr class="table-row"><td class="table-cell">Android version</td><td class="table-cell">5.1.1 (SDK 22)</td></tr>
<tr class="table-row"><td class="table-cell">Webview version</td><td class="table-cell">59.0.3071.125</td></tr>
<tr class="table-row"><td class="table-cell">Device Type</td><td class="table-cell">KFAUWI (Amazon)</td></tr>
<tr class="table-row"><td class="table-cell">Serial</td><td class="table-cell">[REDACTED]</td></tr>
<tr class="table-row"><td class="table-cell">Android ID</td><td class="table-cell">[REDACTED]</td></tr>
</table>
</div></body></html>

2019-01-28 15:56:53 ERROR (SyncWorker_6) [homeassistant.components.sensor.scrape] Unable to extract data from HTML

Having done a quick search, I’ve stumbled upon this page and although it explains the change somewhat, I have no idea how to fix this.
I’ve tried the following variations in my config in an attempt to get anything, but it always comes up with unknown as a value.

- platform: scrape
  resource: !secret fire7url
  name: Fire7 Battery
  select: "td:nth-of-type(44)"
  unit_of_measurement: '%'

- platform: scrape
  resource: !secret fire7url
  name: Fire7 Battery
  select: "tr:nth-of-type(22) td:nth-of-type(2)"
  unit_of_measurement: '%'

- platform: scrape
  resource: !secret fire7url
  name: Fire7 Battery
  select: "tr:nth-of-type(1) td:nth-of-type(1)"
  unit_of_measurement: '%'

- platform: scrape
  resource: !secret fire7url
  name: Fire7 Battery
  select: "td:nth-of-type(1)"
  unit_of_measurement: '%'

In addition, when using the below python script I get the expected output:

from bs4 import BeautifulSoup
import requests
url = ("192.168.0.11:2323/?cmd=deviceInfo&password=9320")
r  = requests.get("http://" +url)
data = r.text
soup = BeautifulSoup(data, "lxml")
print(soup.find_all('td')[43].string)

image

Any ideas?

I don’t know how to fix your scrape sensors, but I would recommend accessing Fully’s REST API instead to retrieve that information.
My configuration looks like this:

sensor:
  # Fire 1
  - platform: rest
    resource: http://<IP Address>:2323/?cmd=deviceInfo&type=json&password=<Fully Password>
    name: Fire 1 Device Info
    value_template: '{{ value_json.hostname4 }}'
    force_update: true
    scan_interval: 120
    json_attributes:
      - appVersionName
      - batteryLevel
      - plugged
      - screenBrightness
  - platform: template
    sensors:
      fire_1_battery:
        friendly_name: Battery
        unit_of_measurement: '%'
        value_template: '{{ states.sensor.fire_1_device_info.attributes["batteryLevel"] }}'
      fire_1_appversion:
        friendly_name: App Version
        value_template: '{{ states.sensor.fire_1_device_info.attributes["appVersionName"] }}'

binary_sensor:
  # Fire 1
  - platform: template
    sensors:
      fire_1_charging:
        friendly_name: Charging
        device_class: plug
        value_template: '{{ states.sensor.fire_1_device_info.attributes["plugged"] }}'
      fire_1_screen:
        friendly_name: Screen
        value_template: "{{ state_attr('sensor.fire_1_device_info', 'screenBrightness') > 50 }}"

homeassistant:
  customize_glob:
    "sensor.fire_*_device_info":
      hidden: true

Thanks. It’s not the answer to my question but is indeed a solution to my problem.

Would still like to know how I can extract data from a table as it’s obviously different than other tags (I have other scrape sensors that work fine), in case someone out there hears me :wink:

1 Like

The problem was solved? How did you do it?
thnaks
David A

Hi,

I am struggling trying the scraping to work.
I use the standard scraping platform.

Trying to read any of the four “o-agreement-product-price”, from : sel | seas-nve.dk

image

using this sensor configuration:

 - platform: scrape
    name: el_price
    resource: https://seas-nve.dk/sel/
    select: "o-agreement-product-price"
    index: 1

I guess my select should be different and I have tried several variation like:

    select: "o-agreement-product-price"

    select: ".o-agreement-product-price"

    select: ".o-agreement-product-price:is(div)"

but all these return empty value

Please any body can help me figure out how I do the select?

Thanks :slight_smile:

I think this won’t work. The HTML page itself does not contain the value, this is how the HTML actually looks like:

        <div class="o-agreement-product-price-container">
          <div class="o-agreement-product-price"></div>
          <div class="o-agreement-product-format">øre/kWh.<br>inkl. moms</div>
        </div>

The value you are looking for must come from somewhere else, maybe JavaScript?

Hi @exxamalte ,

Thanks a lot :slight_smile:
I was erroneously looking at the developer window (F12) instead of the page source.
This explains the reason for my frustration.

One more question.
Can you confirm that if the content/values on a web page, are not part of the static server side composed part og the page, but are created/updated on the client side by for example a java script, then these cannot be scraped because the CSS elements holding this information is not part of the initial version of the page? Do you know of any other way of fetching such information?

Thanks,
Ghassan

1 Like

That really depends on how the site was built. In this particular case we can see the HTML elements and CSS classes, but no value.

Often this information comes in by Javascript fetching JSON data from a different URL. I couldn’t see anything obvious when I briefly looked.

If you can find this URL (if exists) then a rest sensor could be used.

In other cases I have seen the information already embedded with the original request and then populated using JavaScript. That would be much harder to extract.

Hi @exxamalte

Now I’ve got the basics, thanks.

What I really want, is to read the hourly electricity prices for the next day found on this page:

https://seas-nve.dk/kundeservice/aftaler-og-priser/timepris/

And here is the interesting part in the source:

<canvas
    class="hourly-price__chart-canvas"
    data-prices="[&quot;51.630&quot;,&quot;54.001&quot;,&quot;58.658&quot;,&quot;55.915&quot;,&quot;56.771&quot;,&quot;69.830&quot;,&quot;161.784&quot;,&quot;205.703&quot;,&quot;199.131&quot;,&quot;185.998&quot;,&quot;176.908&quot;,&quot;175.439&quot;,&quot;182.205&quot;,&quot;199.596&quot;,&quot;198.090&quot;,&quot;204.718&quot;,&quot;213.556&quot;,&quot;270.033&quot;,&quot;278.900&quot;,&quot;251.108&quot;,&quot;206.679&quot;,&quot;190.691&quot;,&quot;153.549&quot;,&quot;111.471&quot;]"
    data-headings="[&quot;00:00&quot;,&quot;01:00&quot;,&quot;02:00&quot;,&quot;03:00&quot;,&quot;04:00&quot;,&quot;05:00&quot;,&quot;06:00&quot;,&quot;07:00&quot;,&quot;08:00&quot;,&quot;09:00&quot;,&quot;10:00&quot;,&quot;11:00&quot;,&quot;12:00&quot;,&quot;13:00&quot;,&quot;14:00&quot;,&quot;15:00&quot;,&quot;16:00&quot;,&quot;17:00&quot;,&quot;18:00&quot;,&quot;19:00&quot;,&quot;20:00&quot;,&quot;21:00&quot;,&quot;22:00&quot;,&quot;23:00&quot;]"
></canvas>

I want to read the 24 values in the data prices array (corresponding to hours 0…23) into some HA entities.

The values in this example are 51.630, 54.001, 58.658 etc.

Then I can use these to find out when will be the cheapest 3 hours period, and that will determine when I should run my dishwasher.

Can you help me figure out how to extract these values?

Thanks,
Ghassan

This creates a sensor which extracts the values as an array. You can then create template sensors to extract and process the value the way you want?

sensor:
  - platform: scrape
    resource: https://seas-nve.dk/kundeservice/aftaler-og-priser/timepris/
    name: timepris
    select: "canvas"
    attribute: "data-prices"

Hi @exxamalte
This is great thank you very much

I have few more question /need for more help if possible.

The page by default displays the prices for current date, but normally, during the evening, when looking for the time period with cheapest prices with in the next 24 hours, this is some time during the night in the beginning of tomorrow.

On the page there are forward and backward buttons that toggels fetching earlier and future data but this seams to be pure java as I cannot see other than todays data in the source.

Another possibility is that there is a link on the page that downloads the prices for the past 6 days plus tomorrow:

https://seas-nve.dk/?obexport_format=csv&obexport_start=2021-11-01&obexport_end=2021-11-09&obexport_region=east 

The result is a csv file like this:

Date,15:00,16:00,17:00,18:00,19:00,20:00,21:00,22:00,23:00,00:00,01:00,02:00,03:00,04:00,05:00,06:00,07:00,08:00,09:00,10:00,11:00,12:00,13:00,14:00
2021-11-03,156.304,177.766,240.064,255.260,219.615,188.880,170.913,154.444,135.054
2021-11-04,153.928,141.953,135.778,133.165,135.323,148.554,171.018,196.151,198.913,189.205,187.011,168.349,148.916,144.268,154.095,152.580,165.663,186.974,181.460,180.038,153.863,124.268,96.735,89.111
2021-11-05,15.170,14.045,13.794,13.999,14.315,17.644,68.321,93.311,98.201,96.398,93.265,89.314,81.541,79.366,77.349,79.701,81.876,86.209,83.838,77.339,13.980,12.595,11.516,6.961
2021-11-06,3.316,2.666,1.839,1.875,2.061,3.130,2.731,3.316,4.655,9.276,10.531,10.875,11.089,11.163,11.898,12.715,69.955,60.575,32.090,12.566,12.009,11.944,11.200,9.648
2021-11-07,6.403,4.498,3.345,1.671,1.513,2.721,3.196,8.774,10.243,13.236,11.898,13.014,13.293,14.389,14.948,20.990,68.116,78.100,73.508,68.134,68.069,68.088,68.144,68.153
2021-11-08,51.641,54.013,58.670,55.928,56.783,69.845,161.819,205.746,199.174,186.038,176.945,175.476,182.244,199.639,198.133,204.761,213.603,270.090,278.960,251.163,206.723,190.733,153.583,111.495
2021-11-09,91.203,81.320,79.376,77.126,75.583,76.513,125.593,170.368,170.340,110.485,81.050,76.606,74.821,73.946,73.743,73.929,76.179,77.285,76.113,23.864,12.865,13.171,11.070,6.171

As you can see, the start and end dates are part of the url, so if possible we can create the url to include just tomorrow or today and tomorrow

But is there a way of fetching the result into some entity in Home assistant for further processing?

Any other thoughts?

Thank you very much for your help so far :slight_smile:

I cannot see any buttons on the page - are you seeing them when you are logged in?

I am not aware of a way to ingest CSV data directly. I believe most people either create some kind of command line tool that transforms data, or create a custom component.

However, I just found something more promising: I saw that there is a button that lets you select if you are in the west or east of Denmark? If you toggle that then a request is sent off to fetch new data in JSON format :slight_smile:

For example:

You could try using the resulting data with a rest sensor.

Great.
I’ll give it a try and give you an update
Thanks :slight_smile:

Hi @exxamalte ,

Just wanted to give an update.

I used the rest sensor as you pointed out, and I succeeded to get data for today and tomorrow.

Thanks a lot for you help :slight_smile:

So now I have two rest sensors holding the prices for today and tomorrow and are updated automatically every 3 hours.

resource_template: https://seas-nve.dk/wp/wp-admin/admin-ajax.php?action=ob_get_unitprices&current_page=0&now_page=23&date={{ (utcnow().strftime("%s") | float + (24*60*60)) | timestamp_custom("%Y-%m-%d") }}&region=east&skip=0
    scan_interval: 03:00:00
    sensor:
      - name: Nve_prices_rest_tomorrow
        json_attributes:
          - prices
        value_template: "{% for price in value_json.prices %}
            {%- if loop.first %}
            {%- else %},
            {% endif %}
            {{price|round|int}} 
          {%endfor%}"

The response from

https://seas-nve.dk/wp/wp-admin/admin-ajax.php?action=ob_get_unitprices&current_page=0&now_page=23&date=2021-11-23&region=east&skip=0

is

{"prices":["15.373","15.429","16.841","18.831","44.470","83.875","113.055","172.950","264.366","259.495","242.046","239.565","230.706","240.048","196.125","186.159","264.293","271.031","256.985","101.826","90.783","86.236","57.335","14.016"],"headings":["00:00","01:00","02:00","03:00","04:00","05:00","06:00","07:00","08:00","09:00","10:00","11:00","12:00","13:00","14:00","15:00","16:00","17:00","18:00","19:00","20:00","21:00","22:00","23:00"],"date":"Tirsdag 23. november 2021"}

I pick prices and transform the result from a list of strings representing floats (up to 7 characters) into one string of a list of integers (max 3 characters) separated by commas. This reduces the length of the result, otherwise I face the limit of the 255 characters and get errors reading the result.

This gives the following output:

15 , 15 , 17 , 19 , 44 , 84 , 113 , 173 , 264 , 259 , 242 , 240 , 231 , 240 , 196 , 186 , 264 , 271 , 257 , 102 , 91 , 86 , 57 , 14

Next step is that I join the two results into one long string and remove the extra spaces to save space, using a template sensor:

    - name: "nve_prices_rest"
      state: >
        {% set allvalues = states('sensor.nve_prices_rest_today')|regex_replace(find=' ', replace='') + ',' + states('sensor.nve_prices_rest_tomorrow')|regex_replace(find=' ', replace='') %}
        {{ allvalues }}

giving this result:

164,143,129,123,142,153,223,270,279,277,269,246,231,232,239,260,268,297,311,108,91,88,82,42,15,15,17,19,44,84,113,173,264,259,242,240,231,240,196,186,264,271,257,102,91,86,57,14

I can split the string result for one day into an array using template sensor like this

- name: "nve_prices_list_tomorrow"
  state: "{{ states('sensor.nve_prices_rest_tomorrow').split (' ,  ') }}"

Which gives this result

['15', '15', '17', '19', '44', '84', '113', '173', '264', '259', '242', '240', '231', '240', '196', '186', '264', '271', '257', '102', '91', '86', '57', '14']

I can not do that for the 2 days array as the resulting length will be more that 255 characters and this generates sensor error. But never mind, as this limitation does not apply to variables in scripts so I do that within a script instead

Now having the prices for today and tomorrow always available and updated, I made a script that takes period length, and max wait time, (both as number of hours) as input, and calculates the most economical time for start.

Here is the code:

alias: Nve prices update
variables:
  start_index: '{{ now().strftime("%H")|int }}'
  transport_price: 156
  period_length: '{{ input_period_length |int }}'
  max_delay: '{{ input_max_delay | int }}'
  ns: namespace()
sequence:
  - service: input_text.set_value
    target:
      entity_id: input_text.test_sensor
    data:
      value: |
        var: {{ period_length | int }}, del: {{ max_delay | int }}
  - service: input_text.set_value
    data:
      value: |
        {% set nve_rest = states('sensor.nve_prices_rest') %}
        {% set nve_list = nve_rest.split (',') %}

        {% set last_index = 47- period_length |int %}
        {% if nve_list[24] == 'unknown' %}
          {% set last_index = 23-period_length |int%}
        {% endif %}

        {% set max_index = start_index + max_delay - period_length |int %}
        {% if max_index > last_index %}
          {% set max_index = last_index %}
        {% endif %}

        {% set ns = namespace() %}
        {% set ns.result_value = 2000 %}
        {% set ns.result_index = start_index %}
        {% set ns.current_value = 0 %}

        {% for y in range(start_index, max_index + 1) %}
          {% set ns.current_value = 0 %}
          {% for z in range(y, y + period_length |int ) %}
            {% set ns.current_value = ns.current_value + nve_list[z]|int %}
          {% endfor %}
          {% set ns.current_value =  ns.current_value / period_length %} 
          {% if (ns.current_value < ns.result_value) %}   
            {% set ns.result_value = ns.current_value %}
            {% set ns.result_index = y %}
          {% endif %}
        {% endfor %}
        {% if ns.result_index > 23 %}
           {% set ns.result_index = ns.result_index - 24 %}
           {{ (utcnow().strftime("%s") | float + (24*60*60)) | timestamp_custom("%Y-%m-%d") }} {{ ns.result_index }}:00
        {% else %}
           {{ (now().strftime("%Y-%m-%d")) }} {{ ns.result_index }}:00
        {% endif %}          
    target:
      entity_id: input_text.opvask_billig_start
mode: single

And here is how I use it with my dishwasher that supports remote start.

1: I choose program, and for each program I know the length
2: I choose latest desired finish time
3: I give these as input to the script and I get the best start time saved to a text input helper.

4 : I have a sensor that compares current time to target time

opvaskemaskine_ready_to_start:
        value_template: >
          {{ strptime(states('input_text.opvask_billig_start'),"%Y-%m-%d %H:%M").strftime("%s") | float < now().strftime("%s") | float              
          }}
        turn_on:
        turn_off:

5: When this turns on, it triggers an automation that:

  • resets target time
  • activates the dishwasher with selected program

And this seams to work as it should :slight_smile:

If any body get inspired and want to do something similar, please let me know. I would love to share my experience and knowledge on this with you.

Thanks again @exxamalte, I would not have done it without your help :slight_smile:

1 Like