2020-02-08 11:55:27 -07:00
const data = require ( './stub/data' ) ;
const logger = require ( './stub/logger' ) ;
const zigbeeHerdsman = require ( './stub/zigbeeHerdsman' ) ;
const MQTT = require ( './stub/mqtt' ) ;
const settings = require ( '../lib/util/settings' ) ;
const Controller = require ( '../lib/controller' ) ;
const flushPromises = ( ) => new Promise ( setImmediate ) ;
const zigbeeHerdsmanConverters = require ( 'zigbee-herdsman-converters' ) ;
describe ( 'OTA update' , ( ) => {
let controller ;
mockClear = ( mapped ) => {
mapped . ota . updateToLatest = jest . fn ( ) ;
mapped . ota . isUpdateAvailable = jest . fn ( ) ;
}
beforeEach ( async ( ) => {
data . writeDefaultConfiguration ( ) ;
settings . _reRead ( ) ;
data . writeEmptyState ( ) ;
controller = new Controller ( ) ;
await controller . start ( ) ;
await flushPromises ( ) ;
MQTT . publish . mockClear ( ) ;
} ) ;
2020-03-11 13:30:01 -07:00
it ( 'Should subscribe to topics' , async ( ) => {
2020-02-08 11:55:27 -07:00
expect ( MQTT . subscribe ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bridge/ota_update/check' ) ;
expect ( MQTT . subscribe ) . toHaveBeenCalledWith ( 'zigbee2mqtt/bridge/ota_update/update' ) ;
} ) ;
it ( 'Should OTA update a device' , async ( ) => {
const device = zigbeeHerdsman . devices . bulb ;
2020-02-09 12:44:37 -07:00
const endpoint = device . endpoints [ 0 ] ;
let count = 0 ;
endpoint . read . mockImplementation ( ( ) => {
count ++ ;
return { swBuildId : count , dateCode : '2019010' + count }
} ) ;
2020-02-08 11:55:27 -07:00
const mapped = zigbeeHerdsmanConverters . findByZigbeeModel ( device . modelID )
mockClear ( mapped ) ;
logger . info . mockClear ( ) ;
2020-02-09 12:44:37 -07:00
logger . error . mockClear ( ) ;
device . save . mockClear ( ) ;
2020-02-08 11:55:27 -07:00
mapped . ota . updateToLatest . mockImplementationOnce ( ( a , b , onUpdate ) => {
2020-02-09 12:44:37 -07:00
onUpdate ( 0 , null ) ;
onUpdate ( 10 , 3600 ) ;
2020-02-08 11:55:27 -07:00
} ) ;
MQTT . events . message ( 'zigbee2mqtt/bridge/ota_update/update' , 'bulb' ) ;
await flushPromises ( ) ;
2020-02-09 12:44:37 -07:00
expect ( logger . info ) . toHaveBeenCalledWith ( ` Updating 'bulb' to latest firmware ` ) ;
expect ( mapped . ota . isUpdateAvailable ) . toHaveBeenCalledTimes ( 0 ) ;
2020-02-08 11:55:27 -07:00
expect ( mapped . ota . updateToLatest ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mapped . ota . updateToLatest ) . toHaveBeenCalledWith ( device , logger , expect . any ( Function ) ) ;
2020-02-09 12:44:37 -07:00
expect ( logger . info ) . toHaveBeenCalledWith ( ` Update of 'bulb' at 0% ` ) ;
expect ( logger . info ) . toHaveBeenCalledWith ( ` Update of 'bulb' at 10%, +- 60 minutes remaining ` ) ;
expect ( logger . info ) . toHaveBeenCalledWith ( ` Finished update of 'bulb', from '{"softwareBuildID":1,"dateCode":"20190101"}' to '{"softwareBuildID":2,"dateCode":"20190102"}' ` ) ;
expect ( logger . error ) . toHaveBeenCalledTimes ( 0 ) ;
expect ( device . save ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( device . dateCode ) . toBe ( '20190102' ) ;
expect ( device . softwareBuildID ) . toBe ( 2 ) ;
2020-02-08 11:55:27 -07:00
} ) ;
2020-02-09 12:44:37 -07:00
it ( 'Should handle when OTA update fails' , async ( ) => {
2020-02-08 11:55:27 -07:00
const device = zigbeeHerdsman . devices . bulb ;
2020-02-09 12:44:37 -07:00
const endpoint = device . endpoints [ 0 ] ;
endpoint . read . mockImplementation ( ( ) => { return { swBuildId : 1 , dateCode : '2019010' } } ) ;
2020-02-08 11:55:27 -07:00
const mapped = zigbeeHerdsmanConverters . findByZigbeeModel ( device . modelID )
mockClear ( mapped ) ;
logger . info . mockClear ( ) ;
2020-02-09 12:44:37 -07:00
logger . error . mockClear ( ) ;
device . save . mockClear ( ) ;
mapped . ota . updateToLatest . mockImplementationOnce ( ( a , b , onUpdate ) => {
throw new Error ( 'Update failed' ) ;
} ) ;
2020-02-08 11:55:27 -07:00
MQTT . events . message ( 'zigbee2mqtt/bridge/ota_update/update' , 'bulb' ) ;
await flushPromises ( ) ;
2020-02-09 12:44:37 -07:00
expect ( logger . error ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( logger . error ) . toHaveBeenCalledWith ( ` Update of 'bulb' failed (Update failed) ` ) ;
2020-02-08 11:55:27 -07:00
} ) ;
it ( 'Should be able to check if OTA update is available' , async ( ) => {
const device = zigbeeHerdsman . devices . bulb ;
const mapped = zigbeeHerdsmanConverters . findByZigbeeModel ( device . modelID )
mockClear ( mapped ) ;
2020-02-09 12:44:37 -07:00
2020-02-08 11:55:27 -07:00
logger . info . mockClear ( ) ;
mapped . ota . isUpdateAvailable . mockReturnValueOnce ( false ) ;
MQTT . events . message ( 'zigbee2mqtt/bridge/ota_update/check' , 'bulb' ) ;
await flushPromises ( ) ;
expect ( mapped . ota . isUpdateAvailable ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mapped . ota . updateToLatest ) . toHaveBeenCalledTimes ( 0 ) ;
expect ( logger . info ) . toHaveBeenCalledWith ( ` No update available for 'bulb' ` ) ;
2020-02-09 12:44:37 -07:00
logger . info . mockClear ( ) ;
mapped . ota . isUpdateAvailable . mockReturnValueOnce ( true ) ;
MQTT . events . message ( 'zigbee2mqtt/bridge/ota_update/check' , 'bulb' ) ;
await flushPromises ( ) ;
expect ( mapped . ota . isUpdateAvailable ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( mapped . ota . updateToLatest ) . toHaveBeenCalledTimes ( 0 ) ;
expect ( logger . info ) . toHaveBeenCalledWith ( ` Update available for 'bulb' ` ) ;
} ) ;
it ( 'Should handle if OTA update check fails' , async ( ) => {
const device = zigbeeHerdsman . devices . bulb ;
const mapped = zigbeeHerdsmanConverters . findByZigbeeModel ( device . modelID )
mockClear ( mapped ) ;
logger . error . mockClear ( ) ;
mapped . ota . isUpdateAvailable . mockImplementationOnce ( ( ) => { throw new Error ( 'RF singals disturbed because of dogs barking' ) } ) ;
MQTT . events . message ( 'zigbee2mqtt/bridge/ota_update/check' , 'bulb' ) ;
await flushPromises ( ) ;
expect ( mapped . ota . isUpdateAvailable ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mapped . ota . updateToLatest ) . toHaveBeenCalledTimes ( 0 ) ;
expect ( logger . error ) . toHaveBeenCalledWith ( ` Failed to check if update available for 'bulb' (RF singals disturbed because of dogs barking) ` ) ;
2020-02-08 11:55:27 -07:00
} ) ;
it ( 'Should not check for OTA when device does not support it' , async ( ) => {
MQTT . events . message ( 'zigbee2mqtt/bridge/ota_update/check' , 'bulb_color_2' ) ;
await flushPromises ( ) ;
expect ( logger . error ) . toHaveBeenCalledWith ( ` Device 'bulb_color_2' does not support OTA updates ` ) ;
} ) ;
2020-02-09 12:44:37 -07:00
it ( 'Should refuse to check/update when already in progress' , async ( ) => {
jest . useFakeTimers ( ) ;
const device = zigbeeHerdsman . devices . bulb ;
const mapped = zigbeeHerdsmanConverters . findByZigbeeModel ( device . modelID )
mockClear ( mapped ) ;
logger . info . mockClear ( ) ;
mapped . ota . isUpdateAvailable . mockImplementationOnce ( ( ) => {
return new Promise ( ( resolve , reject ) => { setTimeout ( ( ) => resolve ( ) , 99999 ) } )
} ) ;
MQTT . events . message ( 'zigbee2mqtt/bridge/ota_update/check' , 'bulb' ) ;
await flushPromises ( ) ;
MQTT . events . message ( 'zigbee2mqtt/bridge/ota_update/check' , 'bulb' ) ;
await flushPromises ( ) ;
expect ( mapped . ota . isUpdateAvailable ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( logger . error ) . toHaveBeenCalledWith ( ` Update or check already in progress for 'bulb', skipping... ` ) ;
jest . runAllTimers ( ) ;
await flushPromises ( ) ;
} ) ;
2020-02-13 13:10:44 -07:00
it ( 'Shouldnt crash when read modelID after OTA update fails' , async ( ) => {
const device = zigbeeHerdsman . devices . bulb ;
const endpoint = device . endpoints [ 0 ] ;
let count = 0 ;
endpoint . read . mockImplementation ( ( ) => {
if ( count === 1 ) throw new Error ( 'Failed!' )
count ++ ;
return { swBuildId : 1 , dateCode : '2019010' }
} ) ;
const mapped = zigbeeHerdsmanConverters . findByZigbeeModel ( device . modelID )
mockClear ( mapped ) ;
logger . info . mockClear ( ) ;
MQTT . events . message ( 'zigbee2mqtt/bridge/ota_update/update' , 'bulb' ) ;
await flushPromises ( ) ;
expect ( logger . info ) . toHaveBeenCalledWith ( ` Finished update of 'bulb' ` ) ;
} ) ;
2020-02-16 08:00:15 -07:00
it ( 'Should check for update when device requests it' , async ( ) => {
const device = zigbeeHerdsman . devices . bulb ;
const data = { imageType : 12382 } ;
const mapped = zigbeeHerdsmanConverters . findByZigbeeModel ( device . modelID )
mockClear ( mapped ) ;
2020-02-20 12:01:26 -07:00
mapped . ota . isUpdateAvailable . mockReturnValueOnce ( true ) ;
2020-02-16 08:00:15 -07:00
const payload = { data , cluster : 'genOta' , device , endpoint : device . getEndpoint ( 1 ) , type : 'commandQueryNextImageRequest' , linkquality : 10 } ;
2020-02-20 12:01:26 -07:00
logger . info . mockClear ( ) ;
2020-02-16 08:00:15 -07:00
await zigbeeHerdsman . events . message ( payload ) ;
await flushPromises ( ) ;
expect ( mapped . ota . isUpdateAvailable ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mapped . ota . isUpdateAvailable ) . toHaveBeenCalledWith ( device , logger , { "imageType" : 12382 } ) ;
2020-02-28 15:42:59 -07:00
expect ( logger . info ) . toHaveBeenCalledWith ( ` Update available for 'bulb' ` ) ;
2020-02-29 04:43:53 -07:00
expect ( device . endpoints [ 0 ] . commandResponse ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( device . endpoints [ 0 ] . commandResponse ) . toHaveBeenCalledWith ( "genOta" , "queryNextImageResponse" , { "status" : 0x95 } ) ;
2020-02-16 08:00:15 -07:00
// Should not request again when device asks again after a short time
await zigbeeHerdsman . events . message ( payload ) ;
await flushPromises ( ) ;
expect ( mapped . ota . isUpdateAvailable ) . toHaveBeenCalledTimes ( 1 ) ;
2020-02-20 12:01:26 -07:00
const extension = controller . extensions . find ( ( e ) => e . constructor . name === 'OTAUpdate' ) ;
extension . lastChecked = { } ;
logger . info . mockClear ( ) ;
mapped . ota . isUpdateAvailable . mockReturnValueOnce ( false ) ;
await zigbeeHerdsman . events . message ( payload ) ;
await flushPromises ( ) ;
expect ( logger . info ) . not . toHaveBeenCalledWith ( ` Update available for 'bulb' ` )
2020-02-16 08:00:15 -07:00
} ) ;
2020-02-28 15:30:33 -07:00
it ( 'Should respond with NO_IMAGE_AVAILABLE when not supporting OTA' , async ( ) => {
const device = zigbeeHerdsman . devices . bulb _color ;
const data = { imageType : 12382 } ;
const payload = { data , cluster : 'genOta' , device , endpoint : device . getEndpoint ( 1 ) , type : 'commandQueryNextImageRequest' , linkquality : 10 } ;
await zigbeeHerdsman . events . message ( payload ) ;
await flushPromises ( ) ;
2020-02-29 04:43:53 -07:00
expect ( device . endpoints [ 0 ] . commandResponse ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( device . endpoints [ 0 ] . commandResponse ) . toHaveBeenCalledWith ( "genOta" , "queryNextImageResponse" , { "status" : 152 } ) ;
2020-02-28 15:30:33 -07:00
} ) ;
2020-02-08 11:55:27 -07:00
} ) ;