2019-09-09 10:48:09 -07:00
const data = require ( './stub/data' ) ;
const logger = require ( './stub/logger' ) ;
const zigbeeHerdsman = require ( './stub/zigbeeHerdsman' ) ;
const MQTT = require ( './stub/mqtt' ) ;
const path = require ( 'path' ) ;
const mockExit = jest . spyOn ( process , 'exit' ) . mockImplementation ( ( ) => { } ) ;
2018-11-28 11:34:37 -07:00
const settings = require ( '../lib/util/settings' ) ;
2019-09-09 10:48:09 -07:00
const Controller = require ( '../lib/controller' ) ;
2020-09-24 09:06:43 -07:00
const stringify = require ( 'json-stable-stringify-without-jsonify' ) ;
2019-09-09 10:48:09 -07:00
const flushPromises = ( ) => new Promise ( setImmediate ) ;
const tmp = require ( 'tmp' ) ;
const mocksClear = [
zigbeeHerdsman . permitJoin , mockExit , MQTT . end , zigbeeHerdsman . stop , logger . debug ,
MQTT . publish , MQTT . connect , zigbeeHerdsman . devices . bulb _color . removeFromNetwork ,
2019-09-25 03:08:39 -07:00
zigbeeHerdsman . devices . bulb . removeFromNetwork , logger . error ,
2019-09-09 10:48:09 -07:00
] ;
const fs = require ( 'fs' ) ;
2018-11-28 11:34:37 -07:00
describe ( 'Controller' , ( ) => {
let controller ;
beforeEach ( ( ) => {
2019-09-25 16:14:58 -07:00
zigbeeHerdsman . returnDevices . splice ( 0 ) ;
2018-11-28 11:34:37 -07:00
controller = new Controller ( ) ;
2019-09-09 10:48:09 -07:00
mocksClear . forEach ( ( m ) => m . mockClear ( ) ) ;
data . writeDefaultConfiguration ( ) ;
settings . _reRead ( ) ;
data . writeDefaultState ( ) ;
} ) ;
it ( 'Start controller' , async ( ) => {
await controller . start ( ) ;
2020-07-06 12:37:58 -07:00
expect ( zigbeeHerdsman . constructor ) . toHaveBeenCalledWith ( { "network" : { "panID" : 6754 , "extendedPanID" : [ 221 , 221 , 221 , 221 , 221 , 221 , 221 , 221 ] , "channelList" : [ 11 ] , "networkKey" : [ 1 , 3 , 5 , 7 , 9 , 11 , 13 , 15 , 0 , 2 , 4 , 6 , 8 , 10 , 12 , 13 ] } , "databasePath" : path . join ( data . mockDir , "database.db" ) , "databaseBackupPath" : path . join ( data . mockDir , "database.db.backup" ) , "backupPath" : path . join ( data . mockDir , "coordinator_backup.json" ) , "acceptJoiningDeviceHandler" : expect . any ( Function ) , adapter : { concurrent : null } , "serialPort" : { "baudRate" : undefined , "rtscts" : undefined , "path" : "/dev/dummy" } } ) ;
2019-09-09 10:48:09 -07:00
expect ( zigbeeHerdsman . start ) . toHaveBeenCalledTimes ( 1 ) ;
2019-09-30 12:16:00 -07:00
expect ( zigbeeHerdsman . setLED ) . toHaveBeenCalledTimes ( 0 ) ;
2019-11-27 14:02:49 -07:00
expect ( zigbeeHerdsman . setTransmitPower ) . toHaveBeenCalledTimes ( 0 ) ;
2019-09-09 10:48:09 -07:00
expect ( zigbeeHerdsman . permitJoin ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( zigbeeHerdsman . permitJoin ) . toHaveBeenCalledWith ( true ) ;
expect ( logger . info ) . toHaveBeenCalledWith ( ` Currently ${ Object . values ( zigbeeHerdsman . devices ) . length - 1 } devices are joined: ` )
expect ( logger . info ) . toHaveBeenCalledWith ( 'bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (Router)' ) ;
expect ( logger . info ) . toHaveBeenCalledWith ( 'remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch (EndDevice)' ) ;
expect ( logger . info ) . toHaveBeenCalledWith ( '0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)' ) ;
expect ( MQTT . connect ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( MQTT . connect ) . toHaveBeenCalledWith ( "mqtt://localhost" , { "will" : { "payload" : "offline" , "retain" : true , "topic" : "zigbee2mqtt/bridge/state" } } ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bulb' , stringify ( { "state" : "ON" , "brightness" : 50 , "color_temp" : 370 , "linkquality" : 99 } ) , { retain : true , qos : 0 } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/remote' , stringify ( { "brightness" : 255 } ) , { retain : true , qos : 0 } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
2020-06-21 07:36:36 -07:00
it ( 'Start controller when permit join fails' , async ( ) => {
zigbeeHerdsman . permitJoin . mockImplementationOnce ( ( ) => { throw new Error ( "failed!" ) } ) ;
await controller . start ( ) ;
expect ( zigbeeHerdsman . permitJoin ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( MQTT . connect ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
2019-09-09 10:48:09 -07:00
it ( 'Start controller with specific MQTT settings' , async ( ) => {
const ca = tmp . fileSync ( ) . name ;
fs . writeFileSync ( ca , "ca" ) ;
const key = tmp . fileSync ( ) . name ;
fs . writeFileSync ( key , "key" ) ;
const cert = tmp . fileSync ( ) . name ;
fs . writeFileSync ( cert , "cert" ) ;
const configuration = {
base _topic : "zigbee2mqtt" ,
server : "mqtt://localhost" ,
2020-01-17 13:38:46 -07:00
keepalive : 30 ,
2019-09-09 10:48:09 -07:00
ca , cert , key ,
password : 'pass' ,
user : 'user1' ,
client _id : 'my_client_id' ,
reject _unauthorized : false ,
2020-03-12 12:25:37 -07:00
version : 5 ,
2019-09-09 10:48:09 -07:00
}
settings . set ( [ 'mqtt' ] , configuration )
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( MQTT . connect ) . toHaveBeenCalledTimes ( 1 ) ;
const expected = {
"will" : { "payload" : "offline" , "retain" : true , "topic" : "zigbee2mqtt/bridge/state" } ,
2020-01-17 13:38:46 -07:00
keepalive : 30 ,
2019-09-09 10:48:09 -07:00
ca : Buffer . from ( [ 99 , 97 ] ) ,
key : Buffer . from ( [ 107 , 101 , 121 ] ) ,
cert : Buffer . from ( [ 99 , 101 , 114 , 116 ] ) ,
password : 'pass' ,
username : 'user1' ,
clientId : 'my_client_id' ,
rejectUnauthorized : false ,
2020-03-12 12:25:37 -07:00
protocolVersion : 5 ,
2019-09-09 10:48:09 -07:00
}
expect ( MQTT . connect ) . toHaveBeenCalledWith ( "mqtt://localhost" , expected ) ;
} ) ;
2020-04-21 12:58:43 -07:00
it ( 'Should generate network_key when set to GENERATE' , async ( ) => {
settings . set ( [ 'advanced' , 'network_key' ] , 'GENERATE' ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( zigbeeHerdsman . constructor . mock . calls [ 0 ] [ 0 ] . network . networkKey . length ) . toStrictEqual ( 16 ) ;
expect ( data . read ( ) . advanced . network _key . length ) . toStrictEqual ( 16 ) ;
} ) ;
2019-09-25 04:15:30 -07:00
it ( 'Start controller should publish cached states' , async ( ) => {
data . writeDefaultState ( ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb" , stringify ( { "state" : "ON" , "brightness" : 50 , "color_temp" : 370 , "linkquality" : 99 } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/remote" , stringify ( { "brightness" : 255 } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2019-09-25 04:15:30 -07:00
} ) ;
2020-07-10 13:09:16 -07:00
it ( 'Start controller should not publish cached states when disabled' , async ( ) => {
settings . set ( [ 'advanced' , 'cache_state_send_on_startup' ] , false ) ;
data . writeDefaultState ( ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
const publishedTopics = MQTT . publish . mock . calls . map ( m => m [ 0 ] ) ;
expect ( publishedTopics ) . toEqual ( expect . not . arrayContaining ( [ "zigbee2mqtt/bulb" , "zigbee2mqtt/remote" ] ) ) ;
} ) ;
2019-09-25 04:15:30 -07:00
it ( 'Start controller should not publish cached states when cache_state is false' , async ( ) => {
settings . set ( [ 'advanced' , 'cache_state' ] , false ) ;
data . writeDefaultState ( ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( MQTT . publish ) . not . toHaveBeenCalledWith ( "zigbee2mqtt/bulb" , ` {"state":"ON","brightness":50,"color_temp":370,"linkquality":99} ` , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . not . toHaveBeenCalledWith ( "zigbee2mqtt/remote" , ` {"brightness":255} ` , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
} ) ;
2019-09-09 10:48:09 -07:00
it ( 'Log when MQTT client is unavailable' , async ( ) => {
jest . useFakeTimers ( ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
logger . error . mockClear ( ) ;
controller . mqtt . client . reconnecting = true ;
jest . advanceTimersByTime ( 11 * 1000 ) ;
await flushPromises ( ) ;
expect ( logger . error ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( logger . error ) . toHaveBeenCalledWith ( "Not connected to MQTT server!" ) ;
controller . mqtt . client . reconnecting = false ;
} ) ;
it ( 'Dont publish to mqtt when client is unavailable' , async ( ) => {
await controller . start ( ) ;
await flushPromises ( ) ;
logger . error . mockClear ( ) ;
controller . mqtt . client . reconnecting = true ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' , brightness : 50 , color _temp : 370 , color : { r : 100 , g : 50 , b : 10 } , dummy : { 1 : 'yes' , 2 : 'no' } } ) ;
await flushPromises ( ) ;
expect ( logger . error ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( logger . error ) . toHaveBeenCalledWith ( "Not connected to MQTT server!" ) ;
2020-08-13 11:00:35 -07:00
expect ( logger . error ) . toHaveBeenCalledWith ( "Cannot send message: topic: 'zigbee2mqtt/bulb', payload: '{\"brightness\":50,\"color\":{\"b\":10,\"g\":50,\"r\":100},\"color_temp\":370,\"dummy\":{\"1\":\"yes\",\"2\":\"no\"},\"linkquality\":99,\"state\":\"ON\"}" ) ;
2019-09-09 10:48:09 -07:00
controller . mqtt . client . reconnecting = false ;
} ) ;
it ( 'Load empty state when state file does not exist' , async ( ) => {
data . removeState ( ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( controller . state . state ) . toStrictEqual ( { } ) ;
} ) ;
2020-07-15 14:22:32 -07:00
it ( 'Should remove device not on passlist on startup' , async ( ) => {
settings . set ( [ 'passlist' ] , [ zigbeeHerdsman . devices . bulb _color . ieeeAddr ] ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( zigbeeHerdsman . devices . bulb _color . removeFromNetwork ) . toHaveBeenCalledTimes ( 0 ) ;
expect ( zigbeeHerdsman . devices . bulb . removeFromNetwork ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
2019-09-09 10:48:09 -07:00
it ( 'Should remove non whitelisted devices on startup' , async ( ) => {
settings . set ( [ 'whitelist' ] , [ zigbeeHerdsman . devices . bulb _color . ieeeAddr ] ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( zigbeeHerdsman . devices . bulb _color . removeFromNetwork ) . toHaveBeenCalledTimes ( 0 ) ;
expect ( zigbeeHerdsman . devices . bulb . removeFromNetwork ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
2020-07-15 14:22:32 -07:00
it ( 'Should remove device on blocklist on startup' , async ( ) => {
settings . set ( [ 'blocklist' ] , [ zigbeeHerdsman . devices . bulb _color . ieeeAddr ] ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( zigbeeHerdsman . devices . bulb _color . removeFromNetwork ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( zigbeeHerdsman . devices . bulb . removeFromNetwork ) . toHaveBeenCalledTimes ( 0 ) ;
} ) ;
2019-09-09 10:48:09 -07:00
it ( 'Should remove banned devices on startup' , async ( ) => {
settings . set ( [ 'ban' ] , [ zigbeeHerdsman . devices . bulb _color . ieeeAddr ] ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( zigbeeHerdsman . devices . bulb _color . removeFromNetwork ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( zigbeeHerdsman . devices . bulb . removeFromNetwork ) . toHaveBeenCalledTimes ( 0 ) ;
} ) ;
it ( 'Start controller fails' , async ( ) => {
zigbeeHerdsman . start . mockImplementationOnce ( ( ) => { throw new Error ( 'failed' ) } ) ;
await controller . start ( ) ;
expect ( mockExit ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
it ( 'Start controller with permit join true' , async ( ) => {
settings . set ( [ 'permit_join' ] , false ) ;
await controller . start ( ) ;
expect ( zigbeeHerdsman . permitJoin ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( zigbeeHerdsman . permitJoin ) . toHaveBeenCalledWith ( false ) ;
} ) ;
2019-09-30 12:16:00 -07:00
it ( 'Start controller with disable_led' , async ( ) => {
2019-09-09 10:48:09 -07:00
settings . set ( [ 'serial' , 'disable_led' ] , true ) ;
await controller . start ( ) ;
2019-09-30 12:16:00 -07:00
expect ( zigbeeHerdsman . setLED ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( zigbeeHerdsman . setLED ) . toHaveBeenCalledWith ( false ) ;
2019-09-09 10:48:09 -07:00
} ) ;
2019-11-27 14:02:49 -07:00
it ( 'Start controller with transmit power' , async ( ) => {
settings . set ( [ 'experimental' , 'transmit_power' ] , 14 ) ;
await controller . start ( ) ;
expect ( zigbeeHerdsman . setTransmitPower ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( zigbeeHerdsman . setTransmitPower ) . toHaveBeenCalledWith ( 14 ) ;
} ) ;
2019-09-09 10:48:09 -07:00
it ( 'Start controller and stop' , async ( ) => {
await controller . start ( ) ;
await controller . stop ( ) ;
expect ( MQTT . end ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( zigbeeHerdsman . stop ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockExit ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockExit ) . toHaveBeenCalledWith ( 0 ) ;
} ) ;
it ( 'Start controller and stop' , async ( ) => {
zigbeeHerdsman . stop . mockImplementationOnce ( ( ) => { throw new Error ( 'failed' ) } )
await controller . start ( ) ;
await controller . stop ( ) ;
expect ( MQTT . end ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( zigbeeHerdsman . stop ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockExit ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockExit ) . toHaveBeenCalledWith ( 1 ) ;
} ) ;
it ( 'Start controller adapter disconnects' , async ( ) => {
zigbeeHerdsman . stop . mockImplementationOnce ( ( ) => { throw new Error ( 'failed' ) } )
await controller . start ( ) ;
await zigbeeHerdsman . events . adapterDisconnected ( ) ;
await flushPromises ( ) ;
expect ( MQTT . end ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( zigbeeHerdsman . stop ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockExit ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockExit ) . toHaveBeenCalledWith ( 1 ) ;
} ) ;
it ( 'Handle mqtt message' , async ( ) => {
await controller . start ( ) ;
logger . debug . mockClear ( ) ;
await MQTT . events . message ( 'dummytopic' , 'dummymessage' ) ;
expect ( logger . debug ) . toHaveBeenCalledWith ( "Received MQTT message on 'dummytopic' with data 'dummymessage'" )
} ) ;
it ( 'On zigbee event message' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const payload = { device , endpoint : device . getEndpoint ( 1 ) , type : 'attributeReport' , linkquality : 10 , cluster : 'genBasic' , data : { modelId : device . modelID } } ;
await zigbeeHerdsman . events . message ( payload ) ;
await flushPromises ( ) ;
2019-10-01 11:58:08 -07:00
expect ( logger . debug ) . toHaveBeenCalledWith ( ` Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 ` ) ;
2019-09-09 10:48:09 -07:00
} ) ;
it ( 'On zigbee event message with group ID' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const payload = { device , endpoint : device . getEndpoint ( 1 ) , type : 'attributeReport' , linkquality : 10 , groupID : 0 , cluster : 'genBasic' , data : { modelId : device . modelID } } ;
await zigbeeHerdsman . events . message ( payload ) ;
await flushPromises ( ) ;
2019-10-01 11:58:08 -07:00
expect ( logger . debug ) . toHaveBeenCalledWith ( ` Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 with groupID 0 ` ) ;
2019-09-09 10:48:09 -07:00
} ) ;
2020-04-04 10:15:24 -07:00
it ( 'Should add entities which are missing from configuration but are in database to configuration' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . notInSettings ;
expect ( settings . getDevice ( device . ieeeAddr ) ) . not . toBeNull ( ) ;
} ) ;
2019-09-09 10:48:09 -07:00
it ( 'On zigbee event message from unkown device should create it' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . notInSettings ;
2020-04-04 10:15:24 -07:00
settings . removeDevice ( device . ieeeAddr ) ;
2019-09-09 10:48:09 -07:00
expect ( settings . getDevice ( device . ieeeAddr ) ) . toBeNull ( ) ;
const payload = { device , endpoint : device . getEndpoint ( 1 ) , type : 'attributeReport' , linkquality : 10 , groupID : 0 , cluster : 'genBasic' , data : { modelId : device . modelID } } ;
await zigbeeHerdsman . events . message ( payload ) ;
await flushPromises ( ) ;
2019-11-06 12:30:33 -07:00
expect ( settings . getDevice ( device . ieeeAddr ) ) . toStrictEqual ( { "ID" : "0x0017880104e45519" , "friendlyName" : "0x0017880104e45519" , "friendly_name" : "0x0017880104e45519" } ) ;
2019-09-09 10:48:09 -07:00
} ) ;
it ( 'On zigbee deviceJoined' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const payload = { device } ;
await zigbeeHerdsman . events . deviceJoined ( payload ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bridge/log" , stringify ( { "type" : "device_connected" , "message" : { "friendly_name" : "bulb" } } ) , { "retain" : false , qos : 0 } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
2020-07-15 14:22:32 -07:00
it ( 'acceptJoiningDeviceHandler reject device on blocklist' , async ( ) => {
2019-10-17 13:01:39 -07:00
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
2020-07-15 14:22:32 -07:00
settings . set ( [ 'blocklist' ] , [ device . ieeeAddr ] ) ;
2019-10-17 13:01:39 -07:00
const handler = zigbeeHerdsman . constructor . mock . calls [ 0 ] [ 0 ] . acceptJoiningDeviceHandler ;
expect ( await handler ( device . ieeeAddr ) ) . toBe ( false ) ;
} ) ;
2020-07-15 14:22:32 -07:00
it ( 'acceptJoiningDeviceHandler accept device not on blocklist' , async ( ) => {
2019-10-17 13:01:39 -07:00
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
2020-07-15 14:22:32 -07:00
settings . set ( [ 'blocklist' ] , [ '123' ] ) ;
2019-10-17 13:01:39 -07:00
const handler = zigbeeHerdsman . constructor . mock . calls [ 0 ] [ 0 ] . acceptJoiningDeviceHandler ;
expect ( await handler ( device . ieeeAddr ) ) . toBe ( true ) ;
} ) ;
2020-07-15 14:22:32 -07:00
it ( 'acceptJoiningDeviceHandler accept device on passlist' , async ( ) => {
2019-10-17 13:01:39 -07:00
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
2020-07-15 14:22:32 -07:00
settings . set ( [ 'passlist' ] , [ device . ieeeAddr ] ) ;
2019-10-17 13:01:39 -07:00
const handler = zigbeeHerdsman . constructor . mock . calls [ 0 ] [ 0 ] . acceptJoiningDeviceHandler ;
expect ( await handler ( device . ieeeAddr ) ) . toBe ( true ) ;
} ) ;
2020-07-15 14:22:32 -07:00
it ( 'acceptJoiningDeviceHandler reject device not in passlist' , async ( ) => {
2019-10-17 13:01:39 -07:00
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
2020-07-15 14:22:32 -07:00
settings . set ( [ 'passlist' ] , [ '123' ] ) ;
2019-10-17 13:01:39 -07:00
const handler = zigbeeHerdsman . constructor . mock . calls [ 0 ] [ 0 ] . acceptJoiningDeviceHandler ;
expect ( await handler ( device . ieeeAddr ) ) . toBe ( false ) ;
} ) ;
2020-07-15 14:22:32 -07:00
it ( 'acceptJoiningDeviceHandler should prefer passlist above blocklist' , async ( ) => {
2019-10-17 13:01:39 -07:00
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
2020-07-15 14:22:32 -07:00
settings . set ( [ 'passlist' ] , [ device . ieeeAddr ] ) ;
settings . set ( [ 'blocklist' ] , [ device . ieeeAddr ] ) ;
2019-10-17 13:01:39 -07:00
const handler = zigbeeHerdsman . constructor . mock . calls [ 0 ] [ 0 ] . acceptJoiningDeviceHandler ;
expect ( await handler ( device . ieeeAddr ) ) . toBe ( true ) ;
} ) ;
2020-07-15 14:22:32 -07:00
it ( 'acceptJoiningDeviceHandler accept when not on blocklist and passlist' , async ( ) => {
2019-10-26 09:05:40 -07:00
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const handler = zigbeeHerdsman . constructor . mock . calls [ 0 ] [ 0 ] . acceptJoiningDeviceHandler ;
expect ( await handler ( device . ieeeAddr ) ) . toBe ( true ) ;
} ) ;
2019-09-23 13:24:03 -07:00
it ( 'Shouldnt crash when two device join events are received' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const payload = { device } ;
zigbeeHerdsman . events . deviceJoined ( payload ) ;
zigbeeHerdsman . events . deviceJoined ( payload ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bridge/log" , stringify ( { "type" : "device_connected" , "message" : { "friendly_name" : "bulb" } } ) , { "retain" : false , qos : 0 } , expect . any ( Function ) ) ;
2019-09-23 13:24:03 -07:00
} ) ;
2019-09-09 10:48:09 -07:00
it ( 'On zigbee deviceInterview started' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const payload = { device , status : 'started' } ;
await zigbeeHerdsman . events . deviceInterview ( payload ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bridge/log' , stringify ( { "type" : "pairing" , "message" : "interview_started" , "meta" : { "friendly_name" : "bulb" } } ) , { retain : false , qos : 0 } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
it ( 'On zigbee deviceInterview failed' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const payload = { device , status : 'failed' } ;
await zigbeeHerdsman . events . deviceInterview ( payload ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bridge/log' , stringify ( { "type" : "pairing" , "message" : "interview_failed" , "meta" : { "friendly_name" : "bulb" } } ) , { retain : false , qos : 0 } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
it ( 'On zigbee deviceInterview successful supported' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const payload = { device , status : 'successful' } ;
await zigbeeHerdsman . events . deviceInterview ( payload ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bridge/log' , stringify ( { "type" : "pairing" , "message" : "interview_successful" , "meta" : { "friendly_name" : "bulb" , "model" : "LED1545G12" , "vendor" : "IKEA" , "description" : "TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white" , "supported" : true } } ) , { retain : false , qos : 0 } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
it ( 'On zigbee deviceInterview successful not supported' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . unsupported ;
const payload = { device , status : 'successful' } ;
await zigbeeHerdsman . events . deviceInterview ( payload ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bridge/log' , stringify ( { "type" : "pairing" , "message" : "interview_successful" , "meta" : { "friendly_name" : "0x0017880104e45518" , "supported" : false } } ) , { retain : false , qos : 0 } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
it ( 'On zigbee event device announce' , async ( ) => {
await controller . start ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const payload = { device } ;
await zigbeeHerdsman . events . deviceAnnounce ( payload ) ;
await flushPromises ( ) ;
expect ( logger . debug ) . toHaveBeenCalledWith ( ` Device 'bulb' announced itself ` ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bridge/log' , stringify ( { "type" : "device_announced" , "message" : "announce" , "meta" : { "friendly_name" : "bulb" } } ) , { retain : false , qos : 0 } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
2019-09-25 16:14:58 -07:00
it ( 'On zigbee event device leave (removed from database and settings)' , async ( ) => {
2019-09-09 10:48:09 -07:00
await controller . start ( ) ;
2019-09-25 16:14:58 -07:00
zigbeeHerdsman . returnDevices . push ( '0x00124b00120144ae' ) ;
settings . set ( [ 'devices' ] , { } )
MQTT . publish . mockClear ( ) ;
2019-09-09 10:48:09 -07:00
const device = zigbeeHerdsman . devices . bulb ;
const payload = { ieeeAddr : device . ieeeAddr } ;
await zigbeeHerdsman . events . deviceLeave ( payload ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bridge/log' , stringify ( { "type" : "device_removed" , "message" : "left_network" , "meta" : { "friendly_name" : "0x000b57fffec6a5b2" } } ) , { retain : false , qos : 0 } , expect . any ( Function ) ) ;
2019-09-25 16:14:58 -07:00
} ) ;
it ( 'On zigbee event device leave (removed from database and NOT settings)' , async ( ) => {
await controller . start ( ) ;
zigbeeHerdsman . returnDevices . push ( '0x00124b00120144ae' ) ;
const device = zigbeeHerdsman . devices . bulb ;
MQTT . publish . mockClear ( ) ;
const payload = { ieeeAddr : device . ieeeAddr } ;
await zigbeeHerdsman . events . deviceLeave ( payload ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bridge/log' , stringify ( { "type" : "device_removed" , "message" : "left_network" , "meta" : { "friendly_name" : "0x000b57fffec6a5b2" } } ) , { retain : false , qos : 0 } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
it ( 'Publish entity state attribute output' , async ( ) => {
await controller . start ( ) ;
settings . set ( [ 'experimental' , 'output' ] , 'attribute' ) ;
MQTT . publish . mockClear ( ) ;
2020-08-08 07:36:58 -07:00
await controller . publishEntityState ( 'bulb' , { dummy : { 1 : 'yes' , 2 : 'no' } , color : { r : 100 , g : 50 , b : 10 } , state : 'ON' , test : undefined , test1 : null , color _temp : 370 , brightness : 50 } ) ;
2019-09-09 10:48:09 -07:00
await flushPromises ( ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/state" , "ON" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/brightness" , "50" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/color_temp" , "370" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/color" , '100,50,10' , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/dummy-1" , "yes" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/dummy-2" , "no" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-07-27 13:07:03 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/test1" , '' , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/test" , '' , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
2020-01-12 07:07:06 -07:00
it ( 'Publish entity state attribute_json output' , async ( ) => {
await controller . start ( ) ;
settings . set ( [ 'experimental' , 'output' ] , 'attribute_and_json' ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' , brightness : 200 , color _temp : 370 , linkquality : 99 } ) ;
await flushPromises ( ) ;
expect ( MQTT . publish ) . toHaveBeenCalledTimes ( 5 ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/state" , "ON" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/brightness" , "200" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/color_temp" , "370" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/linkquality" , "99" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb" , stringify ( { "state" : "ON" , "brightness" : 200 , "color_temp" : 370 , "linkquality" : 99 } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-01-12 07:07:06 -07:00
} ) ;
2020-03-02 12:08:51 -07:00
it ( 'Publish entity state attribute_json output filtered' , async ( ) => {
await controller . start ( ) ;
settings . set ( [ 'experimental' , 'output' ] , 'attribute_and_json' ) ;
settings . set ( [ 'devices' , zigbeeHerdsman . devices . bulb . ieeeAddr , 'filtered_attributes' ] , [ 'color_temp' , 'linkquality' ] ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' , brightness : 200 , color _temp : 370 , linkquality : 99 } ) ;
await flushPromises ( ) ;
expect ( MQTT . publish ) . toHaveBeenCalledTimes ( 3 ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/state" , "ON" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/brightness" , "200" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb" , stringify ( { "state" : "ON" , "brightness" : 200 } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-03-03 10:30:54 -07:00
} ) ;
it ( 'Publish entity state attribute_json output filtered (device_options)' , async ( ) => {
await controller . start ( ) ;
settings . set ( [ 'experimental' , 'output' ] , 'attribute_and_json' ) ;
settings . set ( [ 'device_options' , 'filtered_attributes' ] , [ 'color_temp' , 'linkquality' ] ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' , brightness : 200 , color _temp : 370 , linkquality : 99 } ) ;
await flushPromises ( ) ;
expect ( MQTT . publish ) . toHaveBeenCalledTimes ( 3 ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/state" , "ON" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb/brightness" , "200" , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb" , stringify ( { "state" : "ON" , "brightness" : 200 } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-03-02 12:08:51 -07:00
} ) ;
2019-09-23 14:59:01 -07:00
it ( 'Publish entity state with device information' , async ( ) => {
2019-09-09 10:48:09 -07:00
await controller . start ( ) ;
settings . set ( [ 'mqtt' , 'include_device_information' ] , true ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' } ) ;
await flushPromises ( ) ;
2020-09-22 08:41:49 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bulb' , stringify ( { "state" : "ON" , "brightness" : 50 , "color_temp" : 370 , "linkquality" : 99 , "device" : { "friendlyName" : "bulb" , "model" : "LED1545G12" , "ieeeAddr" : "0x000b57fffec6a5b2" , "networkAddress" : 40369 , "type" : "Router" , "manufacturerID" : 4476 , "powerSource" : "Mains (single phase)" , "dateCode" : null , "softwareBuildID" : null } } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2019-12-11 12:15:42 -07:00
// Unsupported device should have model "unknown"
await controller . publishEntityState ( 'unsupported2' , { state : 'ON' } ) ;
await flushPromises ( ) ;
2020-09-22 08:41:49 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/unsupported2' , stringify ( { "state" : "ON" , "device" : { "friendlyName" : "unsupported2" , "model" : "unknown" , "ieeeAddr" : "0x0017880104e45529" , "networkAddress" : 6536 , "type" : "EndDevice" , "manufacturerID" : 0 , "powerSource" : "Battery" , "dateCode" : null , "softwareBuildID" : null } } ) , { "qos" : 0 , "retain" : false } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
2020-03-12 12:25:37 -07:00
it ( 'Should publish entity state without retain' , async ( ) => {
await controller . start ( ) ;
settings . set ( [ 'devices' , zigbeeHerdsman . devices . bulb . ieeeAddr , 'retain' ] , false ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' } ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bulb' , stringify ( { "state" : "ON" , "brightness" : 50 , "color_temp" : 370 , "linkquality" : 99 } ) , { "qos" : 0 , "retain" : false } , expect . any ( Function ) ) ;
2020-03-12 12:25:37 -07:00
} ) ;
it ( 'Should publish entity state with retain' , async ( ) => {
await controller . start ( ) ;
settings . set ( [ 'devices' , zigbeeHerdsman . devices . bulb . ieeeAddr , 'retain' ] , true ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' } ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bulb' , stringify ( { "state" : "ON" , "brightness" : 50 , "color_temp" : 370 , "linkquality" : 99 } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-03-12 12:25:37 -07:00
} ) ;
it ( 'Should publish entity state with expiring retention' , async ( ) => {
await controller . start ( ) ;
settings . set ( [ 'mqtt' , 'version' ] , 5 ) ;
settings . set ( [ 'devices' , zigbeeHerdsman . devices . bulb . ieeeAddr , 'retain' ] , true ) ;
settings . set ( [ 'devices' , zigbeeHerdsman . devices . bulb . ieeeAddr , 'retention' ] , 37 ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' } ) ;
await flushPromises ( ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bulb' , stringify ( { "state" : "ON" , "brightness" : 50 , "color_temp" : 370 , "linkquality" : 99 } ) , { "qos" : 0 , "retain" : true , "properties" : { messageExpiryInterval : 37 } } , expect . any ( Function ) ) ;
2020-03-12 12:25:37 -07:00
} ) ;
2019-09-09 10:48:09 -07:00
it ( 'Publish entity state no empty messages' , async ( ) => {
data . writeEmptyState ( ) ;
await controller . start ( ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { } ) ;
await flushPromises ( ) ;
expect ( MQTT . publish ) . toHaveBeenCalledTimes ( 0 ) ;
} ) ;
2020-07-10 13:09:16 -07:00
it ( 'Should allow to disable state persistency' , async ( ) => {
settings . set ( [ 'advanced' , 'cache_state_persistent' ] , false ) ;
data . removeState ( ) ;
await controller . start ( ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' } ) ;
await controller . publishEntityState ( 'bulb' , { brightness : 200 } ) ;
await flushPromises ( ) ;
expect ( MQTT . publish ) . toHaveBeenCalledTimes ( 2 ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb" , stringify ( { state : "ON" } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb" , stringify ( { state : "ON" , brightness : 200 } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2020-07-10 13:09:16 -07:00
await controller . stop ( ) ;
expect ( data . stateExists ( ) ) . toBeFalsy ( ) ;
} ) ;
2020-09-19 01:59:12 -07:00
it ( 'Shouldnt crash when it cannot save state' , async ( ) => {
data . removeState ( ) ;
await controller . start ( ) ;
logger . error . mockClear ( ) ;
controller . state . file = "/" ;
await controller . state . save ( ) ;
expect ( logger . error ) . toHaveBeenCalledWith ( ` Failed to write state to '/' (EISDIR: illegal operation on a directory, open '/') ` ) ;
} ) ;
2019-09-09 10:48:09 -07:00
it ( 'Publish should not cache when set' , async ( ) => {
settings . set ( [ 'advanced' , 'cache_state' ] , false ) ;
data . writeEmptyState ( ) ;
await controller . start ( ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb' , { state : 'ON' } ) ;
await controller . publishEntityState ( 'bulb' , { brightness : 200 } ) ;
await flushPromises ( ) ;
expect ( MQTT . publish ) . toHaveBeenCalledTimes ( 2 ) ;
2020-08-13 11:00:35 -07:00
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb" , stringify ( { "state" : "ON" } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( "zigbee2mqtt/bulb" , stringify ( { "brightness" : 200 } ) , { "qos" : 0 , "retain" : true } , expect . any ( Function ) ) ;
2019-09-09 10:48:09 -07:00
} ) ;
it ( 'Publish should not do anything for unknown entity' , async ( ) => {
await controller . start ( ) ;
MQTT . publish . mockClear ( ) ;
await controller . publishEntityState ( 'bulb-unknown' , { brightness : 200 } ) ;
await flushPromises ( ) ;
expect ( MQTT . publish ) . toHaveBeenCalledTimes ( 0 ) ;
2018-11-28 11:34:37 -07:00
} ) ;
2019-10-03 11:06:31 -07:00
it ( 'Should start when state is corrupted' , async ( ) => {
fs . writeFileSync ( path . join ( data . mockDir , 'state.json' ) , 'corrupted' ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( controller . state . state ) . toStrictEqual ( { } ) ;
} ) ;
2020-04-19 11:08:24 -07:00
it ( 'Load user extension' , async ( ) => {
const extensionPath = path . join ( data . mockDir , 'extension' ) ;
fs . mkdirSync ( extensionPath ) ;
fs . copyFileSync ( path . join ( _ _dirname , 'assets' , 'exampleExtension.js' ) , path . join ( extensionPath , 'exampleExtension.js' ) )
controller = new Controller ( ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
expect ( MQTT . publish ) . toHaveBeenCalledWith ( 'zigbee2mqtt/example/extension' , 'test' , { retain : false , qos : 0 } , expect . any ( Function ) ) ;
} ) ;
2018-11-28 11:34:37 -07:00
} ) ;