[mybmw] Add home-distance channel (#13093)

* add home-distance channel
* use uniformed instead of random gps coordinates

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
This commit is contained in:
Bernd Weymann 2022-07-09 15:56:05 +02:00 committed by GitHub
parent 9c768dc08f
commit c6778fcb00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 111 additions and 52 deletions

View File

@ -339,12 +339,12 @@ GPS location and heading of the vehicle.
* Available for all vehicles with built-in GPS sensor. Function can be enabled/disabled in the head unit
* Read-only values
| Channel Label | Channel ID | Type |
|-----------------|---------------------|--------------|
| GPS Coordinates | gps | Location |
| Heading | heading | Number:Angle |
| Address | address | String |
| Channel Label | Channel ID | Type |
|---------------------|---------------------|---------------|
| GPS Coordinates | gps | Location |
| Heading | heading | Number:Angle |
| Address | address | String |
| Distance from Home | home-distance | Number:Length |
#### Remote Services

View File

@ -114,6 +114,7 @@ public class MyBMWConstants {
public static final String GPS = "gps";
public static final String HEADING = "heading";
public static final String ADDRESS = "address";
public static final String HOME_DISTANCE = "home-distance";
// Status
public static final String DOORS = "doors";

View File

@ -20,6 +20,7 @@ import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
import org.openhab.binding.mybmw.internal.handler.MyBMWCommandOptionProvider;
import org.openhab.binding.mybmw.internal.handler.VehicleHandler;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
@ -42,14 +43,16 @@ import org.osgi.service.component.annotations.Reference;
public class MyBMWHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory;
private final MyBMWCommandOptionProvider commandOptionProvider;
private final LocationProvider locationProvider;
private String localeLanguage;
@Activate
public MyBMWHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference MyBMWCommandOptionProvider cop,
final @Reference LocaleProvider lp) {
final @Reference LocaleProvider localeP, final @Reference LocationProvider locationP) {
httpClientFactory = hcf;
commandOptionProvider = cop;
localeLanguage = lp.getLocale().getLanguage().toLowerCase();
locationProvider = locationP;
localeLanguage = localeP.getLocale().getLanguage().toLowerCase();
}
@Override
@ -63,7 +66,8 @@ public class MyBMWHandlerFactory extends BaseThingHandlerFactory {
if (THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(thingTypeUID)) {
return new MyBMWBridgeHandler((Bridge) thing, httpClientFactory, localeLanguage);
} else if (SUPPORTED_THING_SET.contains(thingTypeUID)) {
VehicleHandler vh = new VehicleHandler(thing, commandOptionProvider, thingTypeUID.getId());
VehicleHandler vh = new VehicleHandler(thing, commandOptionProvider, locationProvider,
thingTypeUID.getId());
return vh;
}
return null;

View File

@ -18,6 +18,6 @@ package org.openhab.binding.mybmw.internal.dto.properties;
* @author Bernd Weymann - Initial contribution
*/
public class Coordinates {
public double latitude;// ": 50.556049,
public double longitude;// ": 8.495669
public double latitude;
public double longitude;
}

View File

@ -20,5 +20,5 @@ package org.openhab.binding.mybmw.internal.dto.properties;
public class Location {
public Coordinates coordinates;
public Address address;
public int heading;// ": 222
public int heading;
}

View File

@ -48,6 +48,7 @@ import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.Converter;
import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@ -55,6 +56,7 @@ import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@ -87,17 +89,22 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
protected String selectedSession = Constants.UNDEF;
protected MyBMWCommandOptionProvider commandOptionProvider;
private LocationProvider locationProvider;
// Data Caches
protected Optional<String> vehicleStatusCache = Optional.empty();
protected Optional<byte[]> imageCache = Optional.empty();
public VehicleChannelHandler(Thing thing, MyBMWCommandOptionProvider cop, String type) {
public VehicleChannelHandler(Thing thing, MyBMWCommandOptionProvider cop, LocationProvider lp, String type) {
super(thing);
commandOptionProvider = cop;
locationProvider = lp;
if (lp.getLocation() == null) {
logger.debug("Home location not available");
}
hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
|| type.equals(VehicleType.ELECTRIC_REX.toString());
|| type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.MILD_HYBRID.toString());
isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
|| type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
isHybrid = hasFuel && isElectric;
@ -448,9 +455,24 @@ public abstract class VehicleChannelHandler extends BaseThingHandler {
}
protected void updatePosition(Location pos) {
updateChannel(CHANNEL_GROUP_LOCATION, GPS, PointType
.valueOf(Double.toString(pos.coordinates.latitude) + "," + Double.toString(pos.coordinates.longitude)));
updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE));
updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, StringType.valueOf(pos.address.formatted));
if (pos.coordinates.latitude < 0) {
updateChannel(CHANNEL_GROUP_LOCATION, GPS, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_LOCATION, HEADING, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, UnDefType.UNDEF);
} else {
PointType vehicleLocation = PointType.valueOf(
Double.toString(pos.coordinates.latitude) + "," + Double.toString(pos.coordinates.longitude));
updateChannel(CHANNEL_GROUP_LOCATION, GPS, vehicleLocation);
updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE));
updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, StringType.valueOf(pos.address.formatted));
PointType homeLocation = locationProvider.getLocation();
if (homeLocation != null) {
updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE,
QuantityType.valueOf(vehicleLocation.distanceFrom(homeLocation).intValue(), SIUnits.METRE));
} else {
updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, UnDefType.UNDEF);
}
}
}
}

View File

@ -30,6 +30,7 @@ import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.Converter;
import org.openhab.binding.mybmw.internal.utils.ImageProperties;
import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
@ -64,8 +65,8 @@ public class VehicleHandler extends VehicleChannelHandler {
ChargeSessionsCallback chargeSessionCallback = new ChargeSessionsCallback();
ByteResponseCallback imageCallback = new ImageCallback();
public VehicleHandler(Thing thing, MyBMWCommandOptionProvider cop, String driveTrain) {
super(thing, cop, driveTrain);
public VehicleHandler(Thing thing, MyBMWCommandOptionProvider cop, LocationProvider lp, String driveTrain) {
super(thing, cop, lp, driveTrain);
}
@Override

View File

@ -238,10 +238,10 @@ public class Converter {
}
if (v.properties.vehicleLocation == null) {
v.properties.vehicleLocation = new Location();
v.properties.vehicleLocation.heading = -1;
v.properties.vehicleLocation.heading = Constants.INT_UNDEF;
v.properties.vehicleLocation.coordinates = new Coordinates();
v.properties.vehicleLocation.coordinates.latitude = -1.234;
v.properties.vehicleLocation.coordinates.longitude = -9.876;
v.properties.vehicleLocation.coordinates.latitude = Constants.INT_UNDEF;
v.properties.vehicleLocation.coordinates.longitude = Constants.INT_UNDEF;
v.properties.vehicleLocation.address = new Address();
v.properties.vehicleLocation.address.formatted = Constants.UNDEF;
}

View File

@ -87,6 +87,8 @@ channel-type.mybmw.front-right-current-channel.label = Tire Pressure Front Right
channel-type.mybmw.front-right-target-channel.label = Tire Pressure Front Right Target
channel-type.mybmw.gps-channel.label = GPS Coordinates
channel-type.mybmw.heading-channel.label = Heading Angle
channel-type.mybmw.home-distance-channel.label = Distance from Home
channel-type.mybmw.home-distance-channel.description = Computed distance between vehicle and home location
channel-type.mybmw.hood-channel.label = Hood
channel-type.mybmw.image-view-channel.label = Image Viewport
channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Front Side View

View File

@ -10,6 +10,7 @@
<channel id="gps" typeId="gps-channel"/>
<channel id="heading" typeId="heading-channel"/>
<channel id="address" typeId="address-channel"/>
<channel id="home-distance" typeId="home-distance-channel"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -16,4 +16,10 @@
<item-type>String</item-type>
<label>Address</label>
</channel-type>
<channel-type id="home-distance-channel">
<item-type>Number:Length</item-type>
<label>Distance from Home</label>
<description>Computed distance between vehicle and home location</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -27,6 +27,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType;
import org.openhab.binding.mybmw.internal.dto.properties.CBS;
import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
import org.openhab.binding.mybmw.internal.handler.VehicleTests;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.binding.mybmw.internal.utils.Converter;
import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
@ -36,6 +37,7 @@ import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.State;
@ -60,7 +62,7 @@ public class StatusWrapper {
public StatusWrapper(String type, String statusJson) {
hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
|| type.equals(VehicleType.ELECTRIC_REX.toString());
|| type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.MILD_HYBRID.toString());
isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
|| type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
isHybrid = hasFuel && isElectric;
@ -246,20 +248,22 @@ public class StatusWrapper {
assertEquals(expected.toString(), dtt.toString(), "Last Update");
break;
case GPS:
assertTrue(state instanceof PointType);
pt = (PointType) state;
assertNotNull(vehicle.properties.vehicleLocation);
assertEquals(
PointType.valueOf(Double.toString(vehicle.properties.vehicleLocation.coordinates.latitude) + ","
+ Double.toString(vehicle.properties.vehicleLocation.coordinates.longitude)),
pt, "Coordinates");
if (state instanceof PointType) {
pt = (PointType) state;
assertNotNull(vehicle.properties.vehicleLocation);
assertEquals(
PointType.valueOf(Double.toString(vehicle.properties.vehicleLocation.coordinates.latitude)
+ "," + Double.toString(vehicle.properties.vehicleLocation.coordinates.longitude)),
pt, "Coordinates");
} // else no check needed
break;
case HEADING:
assertTrue(state instanceof QuantityType);
qt = ((QuantityType) state);
assertEquals(Units.DEGREE_ANGLE, qt.getUnit(), "Angle Unit");
assertNotNull(vehicle.properties.vehicleLocation);
assertEquals(vehicle.properties.vehicleLocation.heading, qt.intValue(), 0.01, "Heading");
if (state instanceof QuantityType) {
qt = ((QuantityType) state);
assertEquals(Units.DEGREE_ANGLE, qt.getUnit(), "Angle Unit");
assertNotNull(vehicle.properties.vehicleLocation);
assertEquals(vehicle.properties.vehicleLocation.heading, qt.intValue(), 0.01, "Heading");
} // else no check needed
break;
case RANGE_RADIUS_ELECTRIC:
assertTrue(state instanceof QuantityType);
@ -568,10 +572,22 @@ public class StatusWrapper {
}
break;
case ADDRESS:
assertTrue(state instanceof StringType);
st = (StringType) state;
assertEquals(st.toFullString(), vehicle.properties.vehicleLocation.address.formatted,
"Location Address");
if (state instanceof StringType) {
st = (StringType) state;
assertEquals(st.toFullString(), vehicle.properties.vehicleLocation.address.formatted,
"Location Address");
} // else no check needed
break;
case HOME_DISTANCE:
if (state instanceof QuantityType) {
qt = (QuantityType) state;
PointType vehicleLocation = PointType
.valueOf(Double.toString(vehicle.properties.vehicleLocation.coordinates.latitude) + ","
+ Double.toString(vehicle.properties.vehicleLocation.coordinates.longitude));
int distance = vehicleLocation.distanceFrom(VehicleTests.HOME_LOCATION).intValue();
assertEquals(qt.intValue(), distance, "Distance from Home");
assertEquals(qt.getUnit(), SIUnits.METRE, "Distance from Home Unit");
} // else no check needed
break;
case RAW:
// don't assert raw channel

View File

@ -62,7 +62,7 @@ public class VehicleStatusTest {
assertEquals("BMW", v.brand, "Car brand");
assertEquals(true, v.properties.areDoorsClosed, "Doors Closed");
assertEquals(76, v.properties.electricRange.distance.value, "Electric Range");
assertEquals(6.789, v.properties.vehicleLocation.coordinates.longitude, 0.1, "Location lon");
assertEquals(9.876, v.properties.vehicleLocation.coordinates.longitude, 0.1, "Location lon");
assertEquals("immediateCharging", v.status.chargingProfile.chargingMode, "Charging Mode");
assertEquals(2, v.status.chargingProfile.getTimerId(2).id, "Timer ID");
assertEquals("[sunday]", v.status.chargingProfile.getTimerId(2).timerWeekDays.toString(), "Timer Weekdays");

View File

@ -28,6 +28,7 @@ import org.openhab.binding.mybmw.internal.VehicleConfiguration;
import org.openhab.binding.mybmw.internal.dto.ChargeStatisticWrapper;
import org.openhab.binding.mybmw.internal.util.FileReader;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
@ -73,7 +74,8 @@ public class ChargeStatisticsTest {
Thing thing = mock(Thing.class);
when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class);
cch = new VehicleHandler(thing, cop, type);
LocationProvider locationProvider = mock(LocationProvider.class);
cch = new VehicleHandler(thing, cop, locationProvider, type);
VehicleConfiguration vc = new VehicleConfiguration();
vc.vin = Constants.ANONYMOUS;
Optional<VehicleConfiguration> ovc = Optional.of(vc);

View File

@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
@ -60,7 +61,8 @@ public class ErrorResponseTest {
Thing thing = mock(Thing.class);
when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class);
cch = new VehicleHandler(thing, cop, type);
LocationProvider locationProvider = mock(LocationProvider.class);
cch = new VehicleHandler(thing, cop, locationProvider, type);
tc = mock(ThingHandlerCallback.class);
cch.setCallback(tc);
channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);

View File

@ -28,6 +28,8 @@ import org.openhab.binding.mybmw.internal.VehicleConfiguration;
import org.openhab.binding.mybmw.internal.dto.StatusWrapper;
import org.openhab.binding.mybmw.internal.util.FileReader;
import org.openhab.binding.mybmw.internal.utils.Constants;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.library.types.PointType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
@ -57,10 +59,10 @@ public class VehicleTests {
private static final int CHECK_AVAILABLE = 3;
private static final int SERVICE_AVAILABLE = 3;
private static final int SERVICE_EMPTY = 3;
private static final int LOCATION = 3;
private static final int LOCATION = 4;
private static final int CHARGE_PROFILE = 44;
private static final int TIRES = 8;
public static final PointType HOME_LOCATION = new PointType("54.321,9.876");
@Nullable
ArgumentCaptor<ChannelUID> channelCaptor;
@Nullable
@ -83,7 +85,9 @@ public class VehicleTests {
Thing thing = mock(Thing.class);
when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class);
cch = new VehicleHandler(thing, cop, type);
LocationProvider locationProvider = mock(LocationProvider.class);
when(locationProvider.getLocation()).thenReturn(HOME_LOCATION);
cch = new VehicleHandler(thing, cop, locationProvider, type);
VehicleConfiguration vc = new VehicleConfiguration();
vc.vin = vin;
Optional<VehicleConfiguration> ovc = Optional.of(vc);
@ -332,9 +336,7 @@ public class VehicleTests {
logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
setup(VehicleType.MILD_HYBRID.toString(), "anonymous");
String content = FileReader.readFileInString("src/test/resources/responses/G21/340i.json");
assertTrue(testVehicle(content, 38, Optional.empty()));
// assertTrue(testVehicle(content,
// STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + LOCATION + TIRES,
// Optional.empty()));
assertTrue(testVehicle(content,
STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty()));
}
}

View File

@ -173,8 +173,8 @@
],
"vehicleLocation": {
"coordinates": {
"latitude": 1.2345,
"longitude": 6.789
"latitude": 54.321,
"longitude": 9.876
},
"address": {
"formatted": "anonymous"