背景

最近在 Home Assistant 中接入了官方集成 Volvo On Call,确实能很方便的获取车辆各种信息以及控制车辆。但有一个问题就是 Volvo On Call 上传的地理坐标是 GCJ-02 火星坐标系,而 Home Assistant 使用的是 WGS84 坐标系,这会导致 Volvo 车辆的坐标产生偏离,比如判定车辆是否在家中都会产生问题。

解决方案

大致流程

  1. 在 Home Assistant 生成一个新的 device tracker
  2. 使用 node-red 轮询 volvo on call 上报的坐标,并经过 js 脚本转换坐标,再调用 device_tracker.see 服务更新第1步中的 device tracker 坐标
  3. 使用第1步中的 device tracker 作为实体在 Home Assistant 中进行地理距离的判定运算等

详细步骤

  1. /config/configuration.yaml 中新增 volvooncall 配置。更多参考官方教程

    volvooncall:
      username: !secret volvooncall_username
      password: !secret volvooncall_password
      region: cn
  2. 重启 Home Assistant 使 volvooncall 集成生效,并观察 /config/known_devices.yaml 中是否多了一个 volvo_xxxx 自动生成的配置,如下

    volvo_xxxxxx:
      name: xxxxxx
      mac:
      icon: mdi:car
      picture:
      track: true
  3. /config/known_devices.yaml 中手动新增一个配置(将用于代替上方的 volvo_xxxxxx )

    volvo_car:
      name: Volvo Car
      mac:
      icon:
      track: true
  4. 重启 Home Assistant,此时你应该能在 开发者工具 中找到第3步手动生成的新实体 device_tracker.volvo_car

    source_type: null
    friendly_name: Volvo Car

    /image/volvo-car-before-update-gps.png

  5. 打开 Node-RED 配置页面,接下来进行相关配置
  6. 新增一个 poll state 节点,用于获取第2步中的 volvo_xxxxxx 地理坐标 /image/volvo-car-node-red-poll-state.png
  7. 新增一个 function 节点,用于计算新的地理坐标,并在 函数 中输入如下代码 /image/volvo-car-node-red-function.png

    var x_PI = 3.14159265358979324 * 3000.0 / 180.0;
    var PI = 3.1415926535897932384626;
    var a = 6378245.0;
    var ee = 0.00669342162296594323;
    /**
     ,* 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02) 的转换
     ,* 即 百度 转 谷歌、高德
     ,* @param bd_lng
     ,* @param bd_lat
     ,* @returns {*[]}
     ,*/
    var bd09togcj02 = function bd09togcj02(bd_lng, bd_lat) {
        var bd_lng = +bd_lng;
        var bd_lat = +bd_lat;
        var x = bd_lng - 0.0065;
        var y = bd_lat - 0.006;
        var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI);
        var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI);
        var gg_lng = z * Math.cos(theta);
        var gg_lat = z * Math.sin(theta);
        return [gg_lng, gg_lat]
    };
    
    /**
     ,* 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换
     ,* 即 谷歌、高德 转 百度
     ,* @param lng
     ,* @param lat
     ,* @returns {*[]}
     ,*/
    var gcj02tobd09 = function gcj02tobd09(lng, lat) {
        var lat = +lat;
        var lng = +lng;
        var z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * x_PI);
        var theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * x_PI);
        var bd_lng = z * Math.cos(theta) + 0.0065;
        var bd_lat = z * Math.sin(theta) + 0.006;
        return [bd_lng, bd_lat]
    };
    
    /**
     ,* WGS-84 转 GCJ-02
     ,* @param lng
     ,* @param lat
     ,* @returns {*[]}
     ,*/
    var wgs84togcj02 = function wgs84togcj02(lng, lat) {
        var lat = +lat;
        var lng = +lng;
        if (out_of_china(lng, lat)) {
            return [lng, lat]
        } else {
            var dlat = transformlat(lng - 105.0, lat - 35.0);
            var dlng = transformlng(lng - 105.0, lat - 35.0);
            var radlat = lat / 180.0 * PI;
            var magic = Math.sin(radlat);
            magic = 1 - ee * magic * magic;
            var sqrtmagic = Math.sqrt(magic);
            dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);
            dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);
            var mglat = lat + dlat;
            var mglng = lng + dlng;
            return [mglng, mglat]
        }
    };
    
    /**
     ,* GCJ-02 转换为 WGS-84
     ,* @param lng
     ,* @param lat
     ,* @returns {*[]}
     ,*/
    var gcj02towgs84 = function gcj02towgs84(lng, lat) {
        var lat = +lat;
        var lng = +lng;
        if (out_of_china(lng, lat)) {
            return [lng, lat]
        } else {
            var dlat = transformlat(lng - 105.0, lat - 35.0);
            var dlng = transformlng(lng - 105.0, lat - 35.0);
            var radlat = lat / 180.0 * PI;
            var magic = Math.sin(radlat);
            magic = 1 - ee * magic * magic;
            var sqrtmagic = Math.sqrt(magic);
            dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);
            dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);
            var mglat = lat + dlat;
            var mglng = lng + dlng;
            return [lng * 2 - mglng, lat * 2 - mglat]
        }
    };
    
    var transformlat = function transformlat(lng, lat) {
        var lat = +lat;
        var lng = +lng;
        var ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
        ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0;
        ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0;
        return ret
    };
    
    var transformlng = function transformlng(lng, lat) {
        var lat = +lat;
        var lng = +lng;
        var ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
        ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0;
        ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0;
        return ret
    };
    
    /**
     ,* 判断是否在国内,不在国内则不做偏移
     ,* @param lng
     ,* @param lat
     ,* @returns {boolean}
     ,*/
    var out_of_china = function out_of_china(lng, lat) {
        var lat = +lat;
        var lng = +lng;
        // 纬度 3.86~53.55, 经度 73.66~135.05
        return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55);
    };
    
    var newLocation = gcj02towgs84(msg.data.attributes.longitude, msg.data.attributes.latitude)
    
    msg.payload = {}
    msg.payload.longitude = newLocation[0]
    msg.payload.latitude = newLocation[1]
    
    return msg;
  8. 新增一个 call service 节点,用于更新第3步中手动创建的实体地理坐标

    Domain: device_tracker
    Service: see
    Data: {
      "dev_id":"volvo_car",
      "gps":[payload.latitude, payload.longitude]
    }
    

    /image/volvo-car-node-red-call-service.png

  9. 将上述3个节点依次连接,并点击“部署”按钮 /image/volvo-car-node-red-line.png
  10. 回到 Home Assistant 的“开发者工具”页面,搜索 device_tracker.volvo_car ,此时就能看到纠偏后的地理坐标 /image/volvo-car-after-update-gps.png

node-red 一键导入

[
    {
        "id": "eaae412a3cb97052",
        "type": "poll-state",
        "z": "b30b6133eb27f27e",
        "name": "",
        "server": "fbec60d9.9bf86",
        "version": 2,
        "exposeToHomeAssistant": false,
        "haConfig": [
            {
                "property": "name",
                "value": ""
            },
            {
                "property": "icon",
                "value": ""
            }
        ],
        "updateinterval": "60",
        "updateIntervalType": "num",
        "updateIntervalUnits": "seconds",
        "outputinitially": false,
        "outputonchanged": false,
        "entity_id": "device_tracker.volvo_xxxxxx",
        "state_type": "str",
        "halt_if": "",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "outputs": 1,
        "x": 250,
        "y": 200,
        "wires": [
            [
                "38ed7f10fa11c589"
            ]
        ]
    },
    {
        "id": "38ed7f10fa11c589",
        "type": "function",
        "z": "b30b6133eb27f27e",
        "name": "",
        "func": "var x_PI = 3.14159265358979324 * 3000.0 / 180.0;\nvar PI = 3.1415926535897932384626;\nvar a = 6378245.0;\nvar ee = 0.00669342162296594323;\n/**\n * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02) 的转换\n * 即 百度 转 谷歌、高德\n * @param bd_lng\n * @param bd_lat\n * @returns {*[]}\n */\nvar bd09togcj02 = function bd09togcj02(bd_lng, bd_lat) {\n    var bd_lng = +bd_lng;\n    var bd_lat = +bd_lat;\n    var x = bd_lng - 0.0065;\n    var y = bd_lat - 0.006;\n    var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI);\n    var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI);\n    var gg_lng = z * Math.cos(theta);\n    var gg_lat = z * Math.sin(theta);\n    return [gg_lng, gg_lat]\n};\n\n/**\n * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换\n * 即 谷歌、高德 转 百度\n * @param lng\n * @param lat\n * @returns {*[]}\n */\nvar gcj02tobd09 = function gcj02tobd09(lng, lat) {\n    var lat = +lat;\n    var lng = +lng;\n    var z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * x_PI);\n    var theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * x_PI);\n    var bd_lng = z * Math.cos(theta) + 0.0065;\n    var bd_lat = z * Math.sin(theta) + 0.006;\n    return [bd_lng, bd_lat]\n};\n\n/**\n * WGS-84 转 GCJ-02\n * @param lng\n * @param lat\n * @returns {*[]}\n */\nvar wgs84togcj02 = function wgs84togcj02(lng, lat) {\n    var lat = +lat;\n    var lng = +lng;\n    if (out_of_china(lng, lat)) {\n        return [lng, lat]\n    } else {\n        var dlat = transformlat(lng - 105.0, lat - 35.0);\n        var dlng = transformlng(lng - 105.0, lat - 35.0);\n        var radlat = lat / 180.0 * PI;\n        var magic = Math.sin(radlat);\n        magic = 1 - ee * magic * magic;\n        var sqrtmagic = Math.sqrt(magic);\n        dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);\n        dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);\n        var mglat = lat + dlat;\n        var mglng = lng + dlng;\n        return [mglng, mglat]\n    }\n};\n\n/**\n * GCJ-02 转换为 WGS-84\n * @param lng\n * @param lat\n * @returns {*[]}\n */\nvar gcj02towgs84 = function gcj02towgs84(lng, lat) {\n    var lat = +lat;\n    var lng = +lng;\n    if (out_of_china(lng, lat)) {\n        return [lng, lat]\n    } else {\n        var dlat = transformlat(lng - 105.0, lat - 35.0);\n        var dlng = transformlng(lng - 105.0, lat - 35.0);\n        var radlat = lat / 180.0 * PI;\n        var magic = Math.sin(radlat);\n        magic = 1 - ee * magic * magic;\n        var sqrtmagic = Math.sqrt(magic);\n        dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);\n        dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);\n        var mglat = lat + dlat;\n        var mglng = lng + dlng;\n        return [lng * 2 - mglng, lat * 2 - mglat]\n    }\n};\n\nvar transformlat = function transformlat(lng, lat) {\n    var lat = +lat;\n    var lng = +lng;\n    var ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));\n    ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;\n    ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0;\n    ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0;\n    return ret\n};\n\nvar transformlng = function transformlng(lng, lat) {\n    var lat = +lat;\n    var lng = +lng;\n    var ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));\n    ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;\n    ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0;\n    ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0;\n    return ret\n};\n\n/**\n * 判断是否在国内,不在国内则不做偏移\n * @param lng\n * @param lat\n * @returns {boolean}\n */\nvar out_of_china = function out_of_china(lng, lat) {\n    var lat = +lat;\n    var lng = +lng;\n    // 纬度 3.86~53.55, 经度 73.66~135.05 \n    return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55);\n};\n\nvar newLocation = gcj02towgs84(msg.data.attributes.longitude, msg.data.attributes.latitude)\n\nmsg.payload = {}\nmsg.payload.longitude = newLocation[0]\nmsg.payload.latitude = newLocation[1]\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 120,
        "y": 320,
        "wires": [
            [
                "8e828fb4ce44b55c"
            ]
        ]
    },
    {
        "id": "8e828fb4ce44b55c",
        "type": "api-call-service",
        "z": "b30b6133eb27f27e",
        "name": "",
        "server": "fbec60d9.9bf86",
        "version": 3,
        "debugenabled": false,
        "service_domain": "device_tracker",
        "service": "see",
        "entityId": "",
        "data": "{   \"dev_id\":\"volvo_car\",   \"gps\":[payload.latitude, payload.longitude] }",
        "dataType": "jsonata",
        "mergecontext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "x": 180,
        "y": 420,
        "wires": [
            []
        ]
    },
    {
        "id": "fbec60d9.9bf86",
        "type": "server",
        "name": "Home Assistant",
        "version": 2,
        "addon": true,
        "rejectUnauthorizedCerts": true,
        "ha_boolean": "y|yes|true|on|home|open",
        "connectionDelay": true,
        "cacheJson": true,
        "heartbeat": false,
        "heartbeatInterval": 30
    }
]